feat: init project

This commit is contained in:
JinmingYang
2023-05-14 16:07:06 +08:00
commit 41e36d2952
64 changed files with 10238 additions and 0 deletions

12
.eslintignore Normal file
View File

@@ -0,0 +1,12 @@
*.global.js
build
node_modules
interactive/guides/vendor/*.md
interactive/data/guides.ts
defaultConfig.ts
packages/preset-icons/src/collections.json
packages/eslint-plugin/fixtures
bench/source/gen*.js
!.vitepress
docs/.vitepress/cache/deps/*.*
test/cases

39
.eslintrc Normal file
View File

@@ -0,0 +1,39 @@
{
"extends": ["@antfu"],
"rules": {
"yml/no-empty-document": "off",
"react/no-unknown-property": "off"
},
"overrides": [
{
"files": [
"playground/**/*.*",
"examples/**/*.*",
"test/fixtures/**/*.*"
],
"rules": {
"no-restricted-imports": "off"
}
},
{
"files": [
"packages/gpt-runner-vscode/**/*.*"
],
"rules": {
"unicorn/prefer-node-protocol": "off"
}
},
{
"files": [
"**/*.md/*.*"
],
"rules": {
"no-restricted-imports": "off",
"no-restricted-syntax": "off",
"no-labels": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-var-requires": "off"
}
}
]
}

20
.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
.cache
.DS_Store
.idea
.nuxt
.output
.temp
*.local
*.log
*.vsix
bench/source/gen*.js
coverage
dist
examples/**/pnpm-lock.yaml
node_modules
packages/gpt-runner/README.md
packages/gpt-runner-vscode/LICENSE
result.json
.svelte-kit
.eslintcache
vite.config.ts.timestamp-*

4
.npmrc Normal file
View File

@@ -0,0 +1,4 @@
ignore-workspace-root-check=true
shamefully-hoist=true
strict-peer-dependencies=false
auto-install-peers=true

16
.tazerc.json Normal file
View File

@@ -0,0 +1,16 @@
{
"exclude": [
"find-up",
"gzip-size",
"@types/vscode",
"codemirror",
"@vue/cli-plugin-babel",
"@vue/cli-service",
"unbuild",
"msw"
],
"packageMode": {
"vue": "minor",
"nuxt": "minor"
}
}

17
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,17 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"prettier.enable": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": false,
"cSpell.words": [
"nicepkg",
"tagify"
],
"workbench.colorCustomizations": {
"activityBar.background": "#22285E",
"titleBar.activeBackground": "#303884",
"titleBar.activeForeground": "#FCFCFE"
}
}

1
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1 @@
Please refer to https://github.com/antfu/contribute

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023-PRESENT Jinming Yang <https://github.com/2214962083>
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

1
README.md Normal file
View File

@@ -0,0 +1 @@
no...

13
alias.ts Normal file
View File

@@ -0,0 +1,13 @@
import { resolve } from 'node:path'
function r(p: string) {
return resolve(__dirname, p)
}
export const alias: Record<string, string> = {
'@nicepkg/gpt-runner': r('./packages/gpt-runner/src/'),
'@nicepkg/gpt-runner-cli': r('./packages/gpt-runner-cli/src/'),
'@nicepkg/gpt-runner-config': r('./packages/gpt-runner-config/src/'),
'@nicepkg/gpt-runner-core': r('./packages/gpt-runner-core/src/'),
'@nicepkg/gpt-runner-shared': r('./packages/gpt-runner-shared/src/'),
}

16
netlify.toml Executable file
View File

@@ -0,0 +1,16 @@
[build.environment]
NODE_VERSION = "18"
NODE_OPTIONS = "--max_old_space_size=4096"
[build]
publish = "docs/dist"
command = "pnpm run deploy"
[functions]
node_bundler = "esbuild"
[[redirects]]
from = "/play/*"
to = "/play/index.html"
status = 200

73
package.json Normal file
View File

@@ -0,0 +1,73 @@
{
"name": "gpt-runner",
"type": "module",
"version": "0.0.1",
"private": true,
"packageManager": "pnpm@8.4.0",
"scripts": {
"taze": "taze minor -wIr && pnpm -r --parallel run update-post",
"build": "rimraf packages/*/dist && esno scripts/copy-files.ts && pnpm -r --filter=./packages/* run build && pnpm -r run build-post",
"dev": "pnpm stub",
"play": "npm -C playground run dev",
"lint": "eslint --cache .",
"lint:fix": "pnpm lint --fix",
"release": "bumpp -r",
"stub": "pnpm -r --filter=./packages/* --parallel run stub",
"typecheck": "tsc --noEmit",
"test": "vitest",
"test:update": "vitest -u",
"test:ci": "pnpm build && pnpm typecheck && pnpm lint && pnpm test"
},
"devDependencies": {
"@antfu/eslint-config": "^0.38.6",
"@types/fs-extra": "^11.0.1",
"@types/node": "^18.16.4",
"@types/prettier": "^2.7.2",
"@types/react": "^18.2.6",
"@nicepkg/gpt-runner": "workspace:*",
"@nicepkg/gpt-runner-cli": "workspace:*",
"@nicepkg/gpt-runner-config": "workspace:*",
"@nicepkg/gpt-runner-core": "workspace:*",
"@nicepkg/gpt-runner-shared": "workspace:*",
"@vitejs/plugin-legacy": "^4.0.3",
"@vitest/ui": "^0.31.0",
"bumpp": "^9.1.0",
"eslint": "8.39.0",
"esno": "^0.16.3",
"execa": "^7.1.1",
"fast-glob": "^3.2.12",
"fs-extra": "^11.1.1",
"jsdom": "^22.0.0",
"lint-staged": "^13.2.2",
"msw": "1.0.1",
"pnpm": "8.4.0",
"prettier": "^2.8.8",
"react": "^18.2.0",
"rollup": "^3.21.5",
"semver": "^7.5.0",
"simple-git-hooks": "^2.8.1",
"taze": "^0.10.1",
"terser": "^5.17.1",
"tsup": "^6.7.0",
"typescript": "^5.0.4",
"unbuild": "^0.8.11",
"unplugin-auto-import": "^0.15.3",
"vite": "^4.3.5",
"vite-plugin-inspect": "^0.7.25",
"vite-plugin-pages": "^0.29.0",
"vitest": "^0.31.0"
},
"pnpm": {
"overrides": {
"magic-string": "^0.30.0"
}
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
},
"lint-staged": {
"*.{js,ts,tsx,vue,md}": [
"eslint --cache --fix"
]
}
}

1
packages/README.md Normal file
View File

@@ -0,0 +1 @@
# Gpt-runner Packages

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023-PRESENT Jinming Yang <https://github.com/2214962083>
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

View File

@@ -0,0 +1 @@
no...

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
'use strict'
import('../dist/cli.mjs')

View File

@@ -0,0 +1,13 @@
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: [
'src/index',
'src/cli',
],
clean: true,
declaration: true,
rollup: {
inlineDependencies: true,
},
})

View File

@@ -0,0 +1,59 @@
{
"name": "@nicepkg/gpt-runner-cli",
"version": "0.0.1",
"description": "CLI for GPT Runner",
"author": {
"name": "Jinming Yang",
"email": "2214962083@qq.com",
"url": "https://github.com/2214962083"
},
"license": "MIT",
"funding": "https://github.com/sponsors/2214962083",
"homepage": "https://github.com/nicepkg/gpt-runner/tree/main/packages/gpt-runner-cli#readme",
"repository": {
"type": "git",
"url": "https://github.com/nicepkg/gpt-runner",
"directory": "packages/cli"
},
"bugs": {
"url": "https://github.com/nicepkg/gpt-runner/issues"
},
"keywords": [],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs"
}
},
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"bin": {
"gptr": "./bin/gpt-runner.mjs"
},
"files": [
"bin",
"dist"
],
"engines": {
"node": ">=14"
},
"scripts": {
"build": "unbuild",
"stub": "unbuild --stub"
},
"dependencies": {
"@ampproject/remapping": "^2.2.1",
"@rollup/pluginutils": "^5.0.2",
"@nicepkg/gpt-runner-config": "workspace:*",
"@nicepkg/gpt-runner-core": "workspace:*",
"cac": "^6.7.14",
"chokidar": "^3.5.3",
"colorette": "^2.0.20",
"consola": "^3.1.0",
"fast-glob": "^3.2.12",
"magic-string": "^0.30.0",
"pathe": "^1.1.0",
"perfect-debounce": "^1.0.0"
}
}

View File

@@ -0,0 +1,48 @@
import { cac } from 'cac'
import { loadConfig } from '@nicepkg/gpt-runner-config'
import { toArray } from '@nicepkg/gpt-runner-core'
import { version } from '../package.json'
import type { CliOptions } from './types'
import { build } from './index'
export async function startCli(cwd = process.cwd(), argv = process.argv, options: CliOptions = {}) {
const cli = cac('gptr')
cli
.command('[...patterns]', 'Glob patterns', {
ignoreOptionDefaultValue: true,
})
.option('-o, --out-file <file>', 'Output file', {
default: cwd,
})
.option('-c, --config [file]', 'Config file')
.option('-w, --watch', 'Watch for file changes')
.option('--preflights', 'Enable preflights', { default: true })
.option('-m, --minify', 'Minify generated CSS', { default: false })
.action(async (patterns: Array<string>, flags) => {
Object.assign(options, {
cwd,
...flags,
})
if (patterns)
options.patterns = patterns
const { config } = await loadConfig(cwd, options.config)
const entries = toArray(config.cli?.entry || options)
await Promise.all(entries.map(entry =>
build({
...options,
...entry,
}),
))
})
cli.help()
cli.version(version)
// Parse CLI args without running the command to
// handle command errors globally
cli.parse(argv, { run: false })
await cli.runMatchedCommand()
}

View File

@@ -0,0 +1,4 @@
import { startCli } from './cli-start'
import { handleError } from './errors'
startCli().catch(handleError)

View File

@@ -0,0 +1,4 @@
import type { UserConfig } from '@nicepkg/gpt-runner-core'
export const defaultConfig: UserConfig = {
}

View File

@@ -0,0 +1,21 @@
import { consola } from 'consola'
export class PrettyError extends Error {
constructor(message: string) {
super(message)
this.name = this.constructor.name
if (typeof Error.captureStackTrace === 'function')
Error.captureStackTrace(this, this.constructor)
else
this.stack = new Error(message).stack
}
}
export function handleError(error: unknown) {
if (error instanceof PrettyError)
consola.error(error.message)
process.exitCode = 1
}

View File

@@ -0,0 +1,150 @@
import { existsSync, promises as fs } from 'node:fs'
import { basename, dirname, normalize, relative, resolve } from 'pathe'
import fg from 'fast-glob'
import { consola } from 'consola'
import { cyan, dim, green } from 'colorette'
import { debounce } from 'perfect-debounce'
import { toArray } from '@nicepkg/gpt-runner-core'
import type { SourceCodeTransformerEnforce, UserConfig } from '@nicepkg/gpt-runner-core'
import { createContext } from '../../shared-integration/src/context'
import { applyTransformers } from '../../shared-integration/src/transformers'
import { version } from '../package.json'
import { defaultConfig } from './config'
import { PrettyError, handleError } from './errors'
import { getWatcher } from './watcher'
import type { CliOptions, ResolvedCliOptions } from './types'
const name = 'gptr'
export async function resolveOptions(options: CliOptions) {
if (!options.patterns?.length) {
throw new PrettyError(
`No glob patterns, try ${cyan(`${name} <path/to/**/*>`)}`,
)
}
return options as ResolvedCliOptions
}
export async function build(_options: CliOptions) {
const fileCache = new Map<string, string>()
const cwd = _options.cwd || process.cwd()
const options = await resolveOptions(_options)
async function loadConfig() {
const ctx = createContext<UserConfig>(options.config, defaultConfig)
const configSources = (await ctx.updateRoot(cwd)).sources.map(i => normalize(i))
return { ctx, configSources }
}
const { ctx, configSources } = await loadConfig()
const files = await fg(options.patterns, { cwd, absolute: true })
await Promise.all(
files.map(async (file) => {
fileCache.set(file, await fs.readFile(file, 'utf8'))
}),
)
consola.log(green(`${name} v${version}`))
consola.start(`GPT Runner ${options.watch ? 'in watch mode...' : 'for production...'}`)
const debouncedBuild = debounce(
async () => {
generate(options).catch(handleError)
},
100,
)
const startWatcher = async () => {
if (!options.watch)
return
const { patterns } = options
const watcher = await getWatcher(options)
if (configSources.length)
watcher.add(configSources)
watcher.on('all', async (type, file) => {
const absolutePath = resolve(cwd, file)
if (configSources.includes(absolutePath)) {
await ctx.reloadConfig()
consola.info(`${cyan(basename(file))} changed, setting new config`)
}
else {
consola.log(`${green(type)} ${dim(file)}`)
if (type.startsWith('unlink'))
fileCache.delete(absolutePath)
else
fileCache.set(absolutePath, await fs.readFile(absolutePath, 'utf8'))
}
debouncedBuild()
})
consola.info(
`Watching for changes in ${
toArray(patterns)
.map(i => cyan(i))
.join(', ')}`,
)
}
await generate(options)
await startWatcher().catch(handleError)
function transformFiles(sources: { id: string; code: string; transformedCode?: string | undefined }[], enforce: SourceCodeTransformerEnforce = 'default') {
return Promise.all(
sources.map(({ id, code, transformedCode }) => new Promise<{ id: string; code: string; transformedCode: string | undefined }>((resolve) => {
applyTransformers(ctx, code, id, enforce)
.then((transformsRes) => {
resolve({ id, code, transformedCode: transformsRes?.code || transformedCode })
})
})))
}
async function generate(options: ResolvedCliOptions) {
const sourceCache = Array.from(fileCache).map(([id, code]) => ({ id, code }))
const outFile = resolve(options.cwd || process.cwd(), options.outFile ?? 'uno.css')
const preTransform = await transformFiles(sourceCache, 'pre')
const defaultTransform = await transformFiles(preTransform)
const postTransform = await transformFiles(defaultTransform, 'post')
// update source file
await Promise.all(
postTransform
.filter(({ transformedCode }) => !!transformedCode)
.map(({ transformedCode, id }) => new Promise<void>((resolve) => {
if (existsSync(id))
fs.writeFile(id, transformedCode as string, 'utf-8').then(resolve)
})),
)
const { css, matched } = await ctx.uno.generate(
[...postTransform.map(({ code, transformedCode }) => transformedCode ?? code)].join('\n'),
{
preflights: options.preflights,
minify: options.minify,
},
)
const dir = dirname(outFile)
if (!existsSync(dir))
await fs.mkdir(dir, { recursive: true })
await fs.writeFile(outFile, css, 'utf-8')
if (!options.watch) {
consola.success(
`${[...matched].length} utilities generated to ${cyan(
relative(process.cwd(), outFile),
)}\n`,
)
}
}
}

View File

@@ -0,0 +1,16 @@
/** Mark some properties as required, leaving others unchanged */
declare type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> & Required<Pick<T, RK>>
export interface CliOptions {
cwd?: string
patterns?: Array<string>
outFile?: string
watch?: boolean
config?: string
// generate options
preflights?: boolean
minify?: boolean
}
export type ResolvedCliOptions = MarkRequired<CliOptions, 'patterns'>

View File

@@ -0,0 +1,22 @@
import type { FSWatcher } from 'chokidar'
import type { CliOptions } from './types'
let watcher: FSWatcher
export async function getWatcher(options?: CliOptions) {
// test case entry without options
if (watcher && !options)
return watcher
const { watch } = await import('chokidar')
const ignored = ['**/{.git,node_modules}/**']
// cli may create multiple watchers
const newWatcher = watch(options?.patterns as string[], {
ignoreInitial: true,
ignorePermissionErrors: true,
ignored,
cwd: options?.cwd || process.cwd(),
})
watcher = newWatcher
return newWatcher
}

View File

@@ -0,0 +1,12 @@
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: [
'src/index',
],
clean: true,
declaration: true,
rollup: {
emitCJS: true,
},
})

View File

@@ -0,0 +1,43 @@
{
"name": "@nicepkg/gpt-runner-config",
"version": "0.0.1",
"description": "Config loader for GPT Runner",
"author": "Jinming Yang <2214962083@qq.com>",
"license": "MIT",
"funding": "https://github.com/sponsors/2214962083",
"homepage": "https://github.com/nicepkg/gpt-runner/tree/main/packages/gpt-runner-config#readme",
"repository": {
"type": "git",
"url": "https://github.com/nicepkg/gpt-runner",
"directory": "packages/gpt-runner-config"
},
"bugs": {
"url": "https://github.com/nicepkg/gpt-runner/issues"
},
"keywords": [],
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.cjs",
"import": "./dist/index.mjs"
}
},
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">=14"
},
"scripts": {
"build": "unbuild",
"stub": "unbuild --stub"
},
"dependencies": {
"@nicepkg/gpt-runner-core": "workspace:*",
"unconfig": "^0.3.7"
}
}

View File

@@ -0,0 +1,68 @@
import { dirname, resolve } from 'node:path'
import fs from 'node:fs'
import type { UserConfig, UserConfigDefaults } from '@nicepkg/gpt-runner'
import type { LoadConfigResult, LoadConfigSource } from 'unconfig'
import { createConfigLoader as createLoader } from 'unconfig'
export type { LoadConfigResult, LoadConfigSource }
export async function loadConfig<U extends UserConfig>(
cwd = process.cwd(),
configOrPath: string | U = cwd,
extraConfigSources: LoadConfigSource[] = [],
defaults: UserConfigDefaults = {},
): Promise<LoadConfigResult<U>> {
let inlineConfig = {} as U
if (typeof configOrPath !== 'string') {
inlineConfig = configOrPath
if (inlineConfig.configFile === false) {
return {
config: inlineConfig as U,
sources: [],
}
}
else {
configOrPath = inlineConfig.configFile || process.cwd()
}
}
const resolved = resolve(configOrPath)
let isFile = false
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
isFile = true
cwd = dirname(resolved)
}
const loader = createLoader<U>({
sources: isFile
? [
{
files: resolved,
extensions: [],
},
]
: [
{
files: [
'gpt-runner.config',
'gpt.config',
],
},
...extraConfigSources,
],
cwd,
defaults: inlineConfig,
})
const result = await loader.load()
result.config = Object.assign(defaults, result.config || inlineConfig)
if (result.config.configDeps) {
result.sources = [
...result.sources,
...result.config.configDeps.map(i => resolve(cwd, i)),
]
}
return result
}

View File

@@ -0,0 +1 @@
no...

View File

@@ -0,0 +1,16 @@
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: [
'src/index',
],
clean: true,
declaration: true,
externals: [
'unconfig',
],
rollup: {
emitCJS: true,
inlineDependencies: true,
},
})

View File

@@ -0,0 +1,44 @@
{
"name": "@nicepkg/gpt-runner-core",
"version": "0.0.1",
"description": "",
"author": "Jinming Yang <2214962083@qq.com>",
"license": "MIT",
"funding": "https://github.com/sponsors/2214962083",
"homepage": "https://github.com/nicepkg/gpt-runner/tree/main/packages/gpt-runner-core#readme",
"repository": {
"type": "git",
"url": "https://github.com/nicepkg/gpt-runner",
"directory": "packages/core"
},
"bugs": {
"url": "https://github.com/nicepkg/gpt-runner/issues"
},
"keywords": [
"gpt-runner",
"chatgpt",
"prompt",
"ai"
],
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.cjs",
"import": "./dist/index.mjs"
}
},
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "unbuild",
"stub": "unbuild --stub"
},
"devDependencies": {
"unconfig": "^0.3.7"
}
}

View File

@@ -0,0 +1 @@
export const pkg = 'core'

View File

@@ -0,0 +1 @@
no...

View File

@@ -0,0 +1,8 @@
{
"name": "@nicepkg/gpt-runner-shared",
"version": "0.0.1",
"private": true,
"dependencies": {
"@nicepkg/gpt-runner-core": "workspace:*"
}
}

View File

@@ -0,0 +1 @@
export const pkg = 'shared'

View File

@@ -0,0 +1,18 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "npm: dev"
}
]
}

View File

@@ -0,0 +1,26 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "dev",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"problemMatcher": [
{
"base": "$ts-webpack-watch",
"background": {
"activeOnStart": true,
"beginsPattern": "Build start",
"endsPattern": "Build success"
}
}
],
"group": "build"
}
]
}

View File

@@ -0,0 +1,4 @@
.github/**
.vscode/**
.vscode-test/**
scripts/**

View File

@@ -0,0 +1 @@
no...

View File

@@ -0,0 +1,78 @@
{
"publisher": "2214962083",
"name": "@nicepkg/gpt-runner-vscode",
"displayName": "GPT Runner",
"version": "0.0.1",
"private": true,
"description": "GPT Runner for VS Code",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/nicepkg/gpt-runner",
"directory": "packages/gpt-runner-vscode"
},
"categories": [
"Other"
],
"main": "./dist/index.js",
"preview": true,
"icon": "res/logo.png",
"engines": {
"vscode": "^1.71.0"
},
"activationEvents": [
"onStartupFinished"
],
"contributes": {
"commands": [
{
"command": "gpt-runner.reload",
"title": "Reload GPT Runner",
"category": "GPT Runner"
}
],
"configuration": {
"type": "object",
"title": "GPT Runner",
"properties": {
"gpt-runner.disable": {
"type": "boolean",
"default": false,
"description": "Disable the GPT Runner extension"
},
"gpt-runner.languageIds": {
"type": [
"array"
],
"items": {
"type": "string"
}
},
"gpt-runner.root": {
"type": [
"array",
"string"
],
"items": {
"type": "string"
},
"description": "Project root that contains the GPT Runner configuration file"
}
}
}
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch src",
"publish": "esno ./scripts/publish.ts"
},
"devDependencies": {
"@types/vscode": "^1.71.0",
"@nicepkg/gpt-runner": "workspace:*",
"esno": "^0.16.3",
"jiti": "^1.18.2",
"prettier": "^2.8.8",
"tsup": "^6.7.0",
"unconfig": "^0.3.7"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,30 @@
/* eslint-disable no-console */
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import fs from 'fs-extra'
import { execa } from 'execa'
const dir = typeof __dirname === 'string' ? __dirname : dirname(fileURLToPath(import.meta.url))
const root = dirname(dir)
async function publish() {
const pkgPath = join(root, 'package.json')
const rawJSON = await fs.readFile(pkgPath, 'utf-8')
const pkg = JSON.parse(rawJSON)
pkg.name = 'gpt-runner'
await fs.writeJSON(pkgPath, pkg, { spaces: 2 })
await execa('npm', ['run', 'build'], { cwd: root, stdio: 'inherit' })
try {
console.log('\nPublish to VSCE...\n')
await execa('npx', ['vsce', 'publish', '--no-dependencies', '-p', process.env.VSCE_TOKEN!], { cwd: root, stdio: 'inherit' })
// console.log('\nPublish to OVSE...\n')
// await execa('npx', ['ovsx', 'publish', '--no-dependencies', '-p', process.env.OVSX_TOKEN!], { cwd: root, stdio: 'inherit' })
}
finally {
await fs.writeFile(pkgPath, rawJSON, 'utf-8')
}
}
publish()

View File

@@ -0,0 +1,180 @@
import path from 'path'
import type { DecorationOptions, ExtensionContext, StatusBarItem } from 'vscode'
import { DecorationRangeBehavior, MarkdownString, Range, window, workspace } from 'vscode'
import { INCLUDE_COMMENT_IDE, getMatchedPositionsFromCode, isCssId } from './integration'
import { log } from './log'
import { getColorString, getPrettiedMarkdown, isSubdir, throttle } from './utils'
import type { ContextLoader } from './contextLoader'
export async function registerAnnotations(
cwd: string,
contextLoader: ContextLoader,
status: StatusBarItem,
ext: ExtensionContext,
) {
let underline: boolean = workspace.getConfiguration().get('gpt-runner.underline') ?? true
let colorPreview: boolean = workspace.getConfiguration().get('gpt-runner.colorPreview') ?? true
ext.subscriptions.push(workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration('gpt-runner.underline')) {
underline = workspace.getConfiguration().get('gpt-runner.underline') ?? true
updateAnnotation()
}
if (event.affectsConfiguration('gpt-runner.colorPreview')) {
colorPreview = workspace.getConfiguration().get('gpt-runner.colorPreview') ?? true
updateAnnotation()
}
}))
workspace.onDidSaveTextDocument(async (doc) => {
const id = doc.uri.fsPath
const dir = path.dirname(id)
if (contextLoader.contextsMap.has(dir)) {
const ctx = contextLoader.contextsMap.get(dir)!
if (!ctx.getConfigFileList().includes(id))
return
try {
await ctx.reloadConfig()
log.appendLine(`🛠 Config reloaded by ${path.relative(cwd, doc.uri.fsPath)}`)
}
catch (e: any) {
log.appendLine('⚠️ Error on loading config')
log.appendLine(String(e.stack ?? e))
}
}
})
const UnderlineDecoration = window.createTextEditorDecorationType({
textDecoration: 'none; border-bottom: 1px dashed currentColor',
rangeBehavior: DecorationRangeBehavior.ClosedClosed,
})
const NoneDecoration = window.createTextEditorDecorationType({
textDecoration: 'none',
rangeBehavior: DecorationRangeBehavior.ClosedClosed,
})
const colorDecoration = window.createTextEditorDecorationType({
before: {
width: '0.9em',
height: '0.9em',
contentText: ' ',
border: '1px solid',
margin: 'auto 0.2em auto 0;vertical-align: middle;border-radius:50%;',
},
dark: {
before: {
borderColor: '#eeeeee50',
},
},
light: {
before: {
borderColor: '#00000050',
},
},
})
async function updateAnnotation(editor = window.activeTextEditor) {
try {
const doc = editor?.document
if (!doc)
return reset()
const id = doc.uri.fsPath
if (!isSubdir(cwd, id))
return reset()
const code = doc.getText()
if (!code)
return reset()
let ctx = await contextLoader.resolveContext(code, id)
if (!ctx)
ctx = await contextLoader.resolveClosestContext(code, id)
const isTarget = ctx.filter(code, id) // normal gpt runner filter
|| code.includes(INCLUDE_COMMENT_IDE) // force include
|| contextLoader.configSources.includes(id) // include config files
|| isCssId(id) // include css files
if (!isTarget)
return reset()
const result = await ctx.uno.generate(code, { id, preflights: false, minify: true })
const colorRanges: DecorationOptions[] = []
const ranges: DecorationOptions[] = (
await Promise.all(
(await getMatchedPositionsFromCode(ctx.uno, code))
.map(async (i): Promise<DecorationOptions> => {
try {
const md = await getPrettiedMarkdown(ctx!.uno, i[2])
if (colorPreview) {
const color = getColorString(md)
if (color && !colorRanges.find(r => r.range.start.isEqual(doc.positionAt(i[0])))) {
colorRanges.push({
range: new Range(doc.positionAt(i[0]), doc.positionAt(i[1])),
renderOptions: { before: { backgroundColor: color } },
})
}
}
return {
range: new Range(doc.positionAt(i[0]), doc.positionAt(i[1])),
get hoverMessage() {
return new MarkdownString(md)
},
}
}
catch (e: any) {
log.appendLine(`⚠️ Failed to parse ${i[2]}`)
log.appendLine(String(e.stack ?? e))
return undefined!
}
}),
)
).filter(Boolean)
editor.setDecorations(colorDecoration, colorRanges)
if (underline) {
editor.setDecorations(NoneDecoration, [])
editor.setDecorations(UnderlineDecoration, ranges)
}
else {
editor.setDecorations(UnderlineDecoration, [])
editor.setDecorations(NoneDecoration, ranges)
}
status.text = `GPT Runner: ${result.matched.size}`
status.tooltip = new MarkdownString(`${result.matched.size} utilities used in this file`)
status.show()
function reset() {
editor?.setDecorations(UnderlineDecoration, [])
editor?.setDecorations(NoneDecoration, [])
editor?.setDecorations(colorDecoration, [])
status.hide()
}
}
catch (e: any) {
log.appendLine('⚠️ Error on annotation')
log.appendLine(String(e.stack ?? e))
}
}
const throttledUpdateAnnotation = throttle(updateAnnotation, 200)
window.onDidChangeActiveTextEditor(updateAnnotation)
workspace.onDidChangeTextDocument((e) => {
if (e.document === window.activeTextEditor?.document)
throttledUpdateAnnotation()
})
contextLoader.events.on('reload', async () => {
await updateAnnotation()
})
await updateAnnotation()
}

View File

@@ -0,0 +1,181 @@
import type { UnocssAutocomplete } from '@unocss/autocomplete'
import { createAutocomplete } from '@unocss/autocomplete'
import type { CompletionItemProvider, Disposable, ExtensionContext } from 'vscode'
import { CompletionItem, CompletionItemKind, CompletionList, MarkdownString, Range, languages, window, workspace } from 'vscode'
import type { UnoGenerator, UnocssPluginContext } from '@unocss/core'
import { getCSS, getColorString, getPrettiedCSS, getPrettiedMarkdown, isSubdir } from './utils'
import { log } from './log'
import type { ContextLoader } from './contextLoader'
import { isCssId } from './integration'
const defaultLanguageIds = [
'erb',
'haml',
'hbs',
'html',
'css',
'postcss',
'javascript',
'javascriptreact',
'markdown',
'ejs',
'php',
'svelte',
'typescript',
'typescriptreact',
'vue-html',
'vue',
'sass',
'scss',
'less',
'stylus',
'astro',
'rust',
]
const delimiters = ['-', ':']
class UnoCompletionItem extends CompletionItem {
uno: UnoGenerator
constructor(label: string, kind: CompletionItemKind, uno: UnoGenerator) {
super(label, kind)
this.uno = uno
}
}
export async function registerAutoComplete(
cwd: string,
contextLoader: ContextLoader,
ext: ExtensionContext,
) {
const allLanguages = await languages.getLanguages()
const autoCompletes = new Map<UnocssPluginContext, UnocssAutocomplete>()
contextLoader.events.on('contextReload', (ctx) => {
autoCompletes.delete(ctx)
})
contextLoader.events.on('contextUnload', (ctx) => {
autoCompletes.delete(ctx)
})
function getAutocomplete(ctx: UnocssPluginContext) {
const cached = autoCompletes.get(ctx)
if (cached)
return cached
const autocomplete = createAutocomplete(ctx.uno)
autoCompletes.set(ctx, autocomplete)
return autocomplete
}
async function getMarkdown(uno: UnoGenerator, util: string) {
return new MarkdownString(await getPrettiedMarkdown(uno, util))
}
function validateLanguages(targets: string[]) {
const unValidLanguages: string[] = []
const validLanguages = targets.filter((language) => {
if (!allLanguages.includes(language)) {
unValidLanguages.push(language)
return false
}
return true
})
if (unValidLanguages.length)
window.showWarningMessage(`These language configurations are illegal: ${unValidLanguages.join(',')}`)
return validLanguages
}
const provider: CompletionItemProvider<UnoCompletionItem> = {
async provideCompletionItems(doc, position) {
const id = doc.uri.fsPath
if (!isSubdir(cwd, id))
return null
const code = doc.getText()
if (!code)
return null
let ctx = await contextLoader.resolveContext(code, id)
if (!ctx)
ctx = await contextLoader.resolveClosestContext(code, id)
if (!ctx.filter(code, id) && !isCssId(id))
return null
try {
const autoComplete = getAutocomplete(ctx)
const result = await autoComplete.suggestInFile(code, doc.offsetAt(position))
log.appendLine(`🤖 ${id} | ${result.suggestions.slice(0, 10).map(v => `[${v[0]}, ${v[1]}]`).join(', ')}`)
if (!result.suggestions.length)
return
const completionItems: UnoCompletionItem[] = []
for (const [value, label] of result.suggestions) {
const css = await getCSS(ctx!.uno, value)
const colorString = getColorString(css)
const itemKind = colorString ? CompletionItemKind.Color : CompletionItemKind.EnumMember
const item = new UnoCompletionItem(value, itemKind, ctx!.uno)
const resolved = result.resolveReplacement(value)
item.insertText = resolved.replacement
item.range = new Range(doc.positionAt(resolved.start), doc.positionAt(resolved.end))
if (colorString) {
item.documentation = colorString
item.sortText = /-\d$/.test(label) ? '1' : '2' // reorder color completions
}
completionItems.push(item)
}
return new CompletionList(completionItems, true)
}
catch (e: any) {
log.appendLine('⚠️ Error on getting autocompletion items')
log.appendLine(String(e.stack ?? e))
return null
}
},
async resolveCompletionItem(item) {
if (item.kind === CompletionItemKind.Color)
item.detail = await (await getPrettiedCSS(item.uno, item.label as string)).prettified
else
item.documentation = await getMarkdown(item.uno, item.label as string)
return item
},
}
let completeUnregister: Disposable
const registerProvider = () => {
completeUnregister?.dispose?.()
const languagesIds: string[] = workspace.getConfiguration().get('unocss.languageIds') || []
const validLanguages = validateLanguages(languagesIds)
completeUnregister = languages.registerCompletionItemProvider(
defaultLanguageIds.concat(validLanguages),
provider,
...delimiters,
)
return completeUnregister
}
ext.subscriptions.push(workspace.onDidChangeConfiguration(async (event) => {
if (event.affectsConfiguration('unocss.languageIds')) {
ext.subscriptions.push(
registerProvider(),
)
}
}))
ext.subscriptions.push(
registerProvider(),
)
}

View File

@@ -0,0 +1,261 @@
import { readdir } from 'fs/promises'
import path from 'path'
import fs from 'fs'
import type { UnocssPluginContext, UserConfig, UserConfigDefaults } from '@unocss/core'
import { notNull } from '@unocss/core'
import { sourceObjectFields, sourcePluginFactory } from 'unconfig/presets'
import presetUno from '@unocss/preset-uno'
import { resolveOptions as resolveNuxtOptions } from '../../nuxt/src/options'
import { createNanoEvents } from '../../core/src/utils/events'
import { createContext, isCssId } from './integration'
import { isSubdir } from './utils'
import { log } from './log'
export class ContextLoader {
public cwd: string
public ready: Promise<void>
public defaultContext: UnocssPluginContext<UserConfig<any>>
public contextsMap = new Map<string, UnocssPluginContext<UserConfig<any>> | null>()
public configSources: string[] = []
private fileContextCache = new Map<string, UnocssPluginContext<UserConfig<any>> | null>()
private configExistsCache = new Map<string, boolean>()
private defaultUnocssConfig: UserConfigDefaults = {
presets: [presetUno()],
}
public events = createNanoEvents<{
reload: () => void
contextLoaded: (context: UnocssPluginContext<UserConfig<any>>) => void
contextReload: (context: UnocssPluginContext<UserConfig<any>>) => void
contextUnload: (context: UnocssPluginContext<UserConfig<any>>) => void
}>()
constructor(cwd: string) {
this.cwd = cwd
this.defaultContext = createContext(this.defaultUnocssConfig)
this.ready = this.reload()
.then(async () => {
await this.defaultContext.ready
})
}
get contexts() {
return Array.from(new Set(this.contextsMap.values())).filter(notNull)
}
async reload() {
this.ready = this._reload()
await this.ready
this.events.emit('reload')
}
private async _reload() {
for (const dir of this.contextsMap.keys())
this.unloadContext(dir)
this.fileContextCache.clear()
this.configExistsCache.clear()
await this.loadContextInDirectory(this.cwd)
}
async unloadContext(configDir: string) {
const context = this.contextsMap.get(configDir)
if (!context)
return
this.contextsMap.delete(configDir)
for (const [path, ctx] of this.fileContextCache) {
if (ctx === context)
this.fileContextCache.delete(path)
}
this.events.emit('contextUnload', context)
}
async configExists(dir: string) {
if (!this.configExistsCache.has(dir)) {
const files = await readdir(dir)
this.configExistsCache.set(dir, files.some(f => /^(vite|svelte|astro|iles|nuxt|unocss|uno)\.config/.test(f)))
}
return this.configExistsCache.get(dir)!
}
async loadContextInDirectory(dir: string) {
const cached = this.contextsMap.get(dir)
if (cached !== undefined)
return cached
const load = async () => {
log.appendLine(`🛠 Resolving config for ${dir}`)
// @ts-expect-error support global utils
globalThis.defineNuxtConfig = config => config
const context = createContext(
dir,
this.defaultUnocssConfig,
[
sourcePluginFactory({
files: [
'vite.config',
'svelte.config',
'iles.config',
],
targetModule: 'unocss/vite',
parameters: [{ command: 'serve', mode: 'development' }],
}),
sourcePluginFactory({
files: [
'astro.config',
],
targetModule: 'unocss/astro',
}),
sourceObjectFields({
files: 'nuxt.config',
fields: 'unocss',
}),
],
(result) => {
if (result.sources.some(s => s.includes('nuxt.config')))
resolveNuxtOptions(result.config)
},
)
context.updateRoot(dir)
let sources = []
try {
sources = (await context.ready).sources
}
catch (e: any) {
log.appendLine(`⚠️ Error on loading config. Config directory: ${dir}`)
log.appendLine(String(e.stack ?? e))
console.error(e)
return null
}
this.configSources = sources
if (!sources.length)
return null
const baseDir = path.dirname(sources[0])
if (baseDir !== dir) {
// exists on upper level, skip
this.contextsMap.set(dir, null)
return null
}
context.onReload(() => {
for (const [path, ctx] of this.fileContextCache) {
if (ctx === context || !ctx)
this.fileContextCache.delete(path)
}
this.configExistsCache.clear()
this.events.emit('contextReload', context)
})
for (const [path, ctx] of this.fileContextCache) {
if (!ctx)
this.fileContextCache.delete(path)
}
this.events.emit('contextLoaded', context)
log.appendLine(`🛠 New configuration loaded from\n${sources.map(s => ` - ${s}`).join('\n')}`)
log.appendLine(` ${context.uno.config.presets.length} presets, ${context.uno.config.rulesSize} rules, ${context.uno.config.shortcuts.length} shortcuts, ${context.uno.config.variants.length} variants, ${context.uno.config.transformers?.length || 0} transformers loaded`)
if (!sources.some(i => i.match(/\buno(css)?\.config\./))) {
log.appendLine('💡 To have the best IDE experience, it\'s recommended to move UnoCSS configurations into a standalone `uno.config.ts` file at the root of your project.')
log.appendLine('👉 Learn more at https://unocss.dev/guide/config-file')
}
return context
}
const context = await load()
if (!this.contextsMap.has(dir))
this.contextsMap.set(dir, context)
return context
}
async resolveContext(code: string, file: string) {
if (file.match(/[\/](node_modules|dist|\.temp|\.cache)[\/]/g))
return
const cached = this.fileContextCache.get(file)
if (cached !== undefined)
return cached
// try finding an existing context that includes the file
for (const [configDir, context] of this.contextsMap) {
if (!context)
continue
if (!isSubdir(configDir, file))
continue
if (!context.filter(code, file) && !isCssId(file))
continue
this.fileContextCache.set(file, context)
return context
}
// try finding a config from disk
if (fs.existsSync(file)) {
let dir = path.dirname(file)
while (isSubdir(this.cwd, dir)) {
if (await this.configExists(dir)) {
const context = await this.loadContextInDirectory(dir)
if (context?.filter(code, file) || isCssId(file)) {
this.fileContextCache.set(file, context)
return context
}
}
const newDir = path.dirname(dir)
if (newDir === dir)
break
dir = newDir
}
}
this.fileContextCache.set(file, null)
return null
}
async resolveClosestContext(code: string, file: string) {
const cached = this.fileContextCache.get(file)
if (cached)
return cached
for (const [configDir, context] of this.contextsMap) {
if (!context)
continue
if (!isSubdir(configDir, file))
continue
if (!context.filter(code, file) && !isCssId(file))
continue
this.fileContextCache.set(file, context)
return context
}
for (const [configDir, context] of this.contextsMap) {
if (!context)
continue
if (isSubdir(configDir, file)) {
this.fileContextCache.set(file, context)
return context
}
}
return this.defaultContext
}
}

View File

@@ -0,0 +1,90 @@
import path from 'path'
import type { ExtensionContext, StatusBarItem } from 'vscode'
import { StatusBarAlignment, commands, window, workspace } from 'vscode'
import { version } from '../package.json'
import { log } from './log'
import { registerAnnotations } from './annotation'
import { registerAutoComplete } from './autocomplete'
import { ContextLoader } from './contextLoader'
import { registerSelectionStyle } from './selectionStyle'
async function registerRoot(ext: ExtensionContext, status: StatusBarItem, cwd: string) {
const contextLoader = new ContextLoader(cwd)
await contextLoader.ready
const hasConfig = await contextLoader.loadContextInDirectory(cwd)
// TODO: improve this to re-enable after configuration created
if (!hasConfig)
throw new Error(` No config found in ${cwd}`)
registerAutoComplete(cwd, contextLoader, ext)
registerAnnotations(cwd, contextLoader, status, ext)
registerSelectionStyle(cwd, contextLoader)
return contextLoader
}
export async function activate(ext: ExtensionContext) {
log.appendLine(`⚪️ UnoCSS for VS Code v${version}\n`)
const projectPath = workspace.workspaceFolders?.[0].uri.fsPath
if (!projectPath) {
log.appendLine(' No active workspace found, UnoCSS is disabled')
return
}
const config = workspace.getConfiguration('unocss')
const disabled = config.get<boolean>('disable', false)
if (disabled) {
log.appendLine(' Disabled by configuration')
return
}
const status = window.createStatusBarItem(StatusBarAlignment.Right, 200)
status.text = 'UnoCSS'
const root = config.get<string | string[]>('root')
if (Array.isArray(root) && root.length) {
const cwds = root.map(dir => path.resolve(projectPath, dir))
try {
const contextLoaders = await Promise.all(cwds.map(cwd => registerRoot(ext, status, cwd)))
ext.subscriptions.push(
commands.registerCommand('unocss.reload', async () => {
log.appendLine('🔁 Reloading...')
await Promise.all(contextLoaders.map(ctxLoader => ctxLoader.reload))
log.appendLine('✅ Reloaded.')
}),
)
}
catch (e: any) {
log.appendLine(String(e.stack ?? e))
}
return
}
// now if the root is an array, then it is an empty array
const cwd = (root && !Array.isArray(root))
? path.resolve(projectPath, root)
: projectPath
try {
const contextLoader = await registerRoot(ext, status, cwd)
ext.subscriptions.push(
commands.registerCommand('unocss.reload', async () => {
log.appendLine('🔁 Reloading...')
await contextLoader.reload()
log.appendLine('✅ Reloaded.')
}),
)
}
catch (e: any) {
log.appendLine(String(e.stack ?? e))
}
}
export function deactivate() {}

View File

@@ -0,0 +1,2 @@
export * from '../../shared-integration/src'
export * from '../../shared-common/src'

View File

@@ -0,0 +1,3 @@
import { window } from 'vscode'
export const log = window.createOutputChannel('UnoCSS')

View File

@@ -0,0 +1,95 @@
import { MarkdownString, Position, Range, window, workspace } from 'vscode'
import parserCSS from 'prettier/parser-postcss'
import prettier from 'prettier/standalone'
import type { TextEditorSelectionChangeEvent } from 'vscode'
import { TwoKeyMap, regexScopePlaceholder } from '@unocss/core'
import { log } from './log'
import { throttle } from './utils'
import type { ContextLoader } from './contextLoader'
import { getMatchedPositionsFromCode } from './integration'
export async function registerSelectionStyle(cwd: string, contextLoader: ContextLoader) {
const hasSelectionStyle = (): boolean => workspace.getConfiguration().get('unocss.selectionStyle') ?? true
const integrationDecoration = window.createTextEditorDecorationType({})
async function selectionStyle(editor: TextEditorSelectionChangeEvent) {
try {
if (!hasSelectionStyle())
return reset()
const doc = editor.textEditor.document
if (!doc)
return reset()
const id = doc.uri.fsPath
const selection = editor.textEditor.selection
const range = new Range(
new Position(selection.start.line, selection.start.character),
new Position(selection.end.line, selection.end.character),
)
let code = editor.textEditor.document.getText(range).trim()
if (!code.startsWith('<'))
code = `<div ${code}`
if (!code.endsWith('>'))
code = `${code} >`
const ctx = await contextLoader.resolveContext(code, id) || (await contextLoader.resolveClosestContext(code, id))
const result = await getMatchedPositionsFromCode(ctx.uno, code)
if (result.length <= 1)
return reset()
const uniqMap = new Map<string, string>()
for (const [start, end, className] of result)
uniqMap.set(`${start}-${end}`, className)
const classNamePlaceholder = '___'
const sheetMap = new TwoKeyMap<string | undefined, string, string>()
await Promise.all(Array.from(uniqMap.values())
.map(async (name) => {
const tokens = await ctx.uno.parseToken(name, classNamePlaceholder) || []
tokens.forEach(([, className, cssText, media]) => {
if (className && cssText) {
const selector = className
.replace(`.${classNamePlaceholder}`, '&')
.replace(regexScopePlaceholder, ' ')
.trim()
sheetMap.set(media, selector, (sheetMap.get(media, selector) || '') + cssText)
}
})
}),
)
const css = Array.from(sheetMap._map.entries())
.map(([media, map]) => {
const body = Array.from(map.keys())
.sort()
.map(selector => `${selector}{${map.get(selector)}}`)
.join('\n')
return media ? `${media}{${body}}` : body
})
.join('\n')
const prettified = prettier.format(css, {
parser: 'css',
plugins: [parserCSS],
})
editor.textEditor.setDecorations(integrationDecoration, [{
range,
get hoverMessage() {
return new MarkdownString(`UnoCSS utilities in the selection will be equivalent to:\n\`\`\`css\n${prettified.trim()}\n\`\`\``)
},
}])
function reset() {
editor.textEditor.setDecorations(integrationDecoration, [])
}
}
catch (e: any) {
log.appendLine('⚠️ Error on selectionStyle')
log.appendLine(String(e.stack ?? e))
}
}
window.onDidChangeTextEditorSelection(throttle(selectionStyle, 200))
}

View File

@@ -0,0 +1,124 @@
import path from 'path'
import type { UnoGenerator } from '@unocss/core'
import prettier from 'prettier/standalone'
import parserCSS from 'prettier/parser-postcss'
export function throttle<T extends ((...args: any) => any)>(func: T, timeFrame: number): T {
let lastTime = 0
let timer: any
return function (...args) {
const now = Date.now()
clearTimeout(timer)
if (now - lastTime >= timeFrame) {
lastTime = now
return func(...args)
}
else {
timer = setTimeout(func, timeFrame, ...args)
}
} as T
}
export async function getCSS(uno: UnoGenerator, utilName: string) {
const { css } = await uno.generate(utilName, { preflights: false, safelist: false })
return css
}
export async function getPrettiedCSS(uno: UnoGenerator, util: string) {
const result = (await uno.generate(new Set([util]), { preflights: false, safelist: false }))
const prettified = prettier.format(result.css, {
parser: 'css',
plugins: [parserCSS],
})
return {
...result,
prettified,
}
}
export async function getPrettiedMarkdown(uno: UnoGenerator, util: string) {
return `\`\`\`css\n${(await getPrettiedCSS(uno, util)).prettified}\n\`\`\``
}
function getCssVariables(code: string) {
const regex = /(?<key>--\S+?):\s*(?<value>.+?);/gm
const cssVariables = new Map<string, string>()
for (const match of code.matchAll(regex)) {
const key = match.groups?.key
if (key)
cssVariables.set(key, match.groups?.value ?? '')
}
return cssVariables
}
const matchCssVarNameRegex = /var\((?<cssVarName>--[^,|)]+)(?:,\s*(?<fallback>[^)]+))?\)/gm
const cssColorRegex = /(?:#|0x)(?:[a-f0-9]{3}|[a-f0-9]{6})\b|(?:rgb|hsl)a?\(.*\)/gm
/**
* Get CSS color string from CSS string
*
* @example Input with CSS var
* ```css
*.dark [border="dark\:gray-700"] {
* --un-border-opacity: 1;
* border-color: rgba(55, 65, 81, var(--un-border-opacity));
*}
* ```
* return `rgba(55, 65, 81, 1)`
*
* @example Input with no-value CSS var and its fallback value
* ```css
*.bg-brand-primary {
* background-color: hsla(217, 78%, 51%, var(--no-value, 0.5));
*}
* ```
* return `hsla(217, 78%, 51%, 0.5)`
*
* @example Input with no-value CSS var
* ```css
*.bg-brand-primary {
* background-color: hsla(217, 78%, 51%, var(--no-value));
*}
* ```
* return `hsla(217, 78%, 51%)`
*
* @param str - CSS string
* @returns The **first** CSS color string (hex, rgb[a], hsl[a]) or `undefined`
*/
export function getColorString(str: string) {
let colorString = str.match(cssColorRegex)?.[0] // e.g rgba(248, 113, 113, var(--maybe-css-var))
if (!colorString)
return
const cssVars = getCssVariables(str)
// replace `var(...)` with its value
for (const match of colorString.matchAll(matchCssVarNameRegex)) {
const matchedString = match[0]
const cssVarName = match.groups?.cssVarName
const fallback = match.groups?.fallback
if (cssVarName && cssVars.get(cssVarName))
// rgba(248, 113, 113, var(--un-text-opacity)) => rgba(248, 113, 113, 1)
colorString = colorString.replaceAll(matchedString, cssVars.get(cssVarName) ?? matchedString)
else if (fallback)
// rgba(248, 113, 113, var(--no-value, 0.5)) => rgba(248, 113, 113, 0.5)
colorString = colorString.replaceAll(matchedString, fallback)
// rgba(248, 113, 113, var(--no-value)) => rgba(248, 113, 113)
colorString = colorString.replaceAll(/,?\s+var\(--.*?\)/gm, '')
}
// if (!(new TinyColor(colorString).isValid))
// return
return colorString
}
export function isSubdir(parent: string, child: string) {
const relative = path.relative(parent, child)
return relative && !relative.startsWith('..') && !path.isAbsolute(relative)
}

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: [
'src/index.ts',
],
external: [
'vscode',
],
format: [
'cjs',
],
shims: false,
})

View File

@@ -0,0 +1,14 @@
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: [
'src/index',
],
clean: true,
declaration: true,
externals: [
],
rollup: {
emitCJS: true,
},
})

2
packages/gpt-runner/index.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export * from './dist/index'
export { default } from './dist/index'

View File

@@ -0,0 +1,50 @@
{
"name": "@nicepkg/gpt-runner",
"version": "0.0.1",
"description": "",
"author": "Jinming Yang <2214962083@qq.com>",
"license": "MIT",
"funding": "https://github.com/sponsors/2214962083",
"homepage": "https://github.com/nicepkg/gpt-runner#readme",
"repository": {
"type": "git",
"url": "https://github.com/nicepkg/gpt-runner"
},
"sponsor": {
"url": "https://github.com/sponsors/2214962083"
},
"bugs": {
"url": "https://github.com/nicepkg/gpt-runner/issues"
},
"keywords": [
"gpt-runner",
"chatgpt",
"prompt",
"ai"
],
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.cjs",
"import": "./dist/index.mjs"
}
},
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "index.d.ts",
"files": [
"dist",
"*.d.ts"
],
"engines": {
"node": ">=14"
},
"scripts": {
"build": "unbuild",
"stub": "unbuild --stub"
},
"dependencies": {
"@nicepkg/gpt-runner-core": "workspace:*"
}
}

View File

@@ -0,0 +1,7 @@
import type { UserConfig } from '@nicepkg/gpt-runner-core'
export * from '@nicepkg/gpt-runner-core'
export function defineConfig(config: UserConfig) {
return config
}

11
playground/package.json Normal file
View File

@@ -0,0 +1,11 @@
{
"name": "playground",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite --open",
"build": "vite build",
"update-post": "vite build",
"serve": "vite preview"
}
}

8145
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

5
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,5 @@
packages:
- docs
- playground
- 'packages/*'
- 'test/fixtures/*'

14
scripts/copy-files.ts Normal file
View File

@@ -0,0 +1,14 @@
import { copyFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
// relative to scripts directory
const destinations = [
['../LICENSE', '../packages/gpt-runner-vscode/LICENSE'],
['../README.md', '../packages/gpt-runner-ui/README.md'],
]
const _filename = fileURLToPath(import.meta.url)
destinations.forEach(([src, dest]) => {
copyFileSync(resolve(_filename, '..', src), resolve(_filename, '..', dest))
})

26
scripts/prepare.ts Normal file
View File

@@ -0,0 +1,26 @@
import { basename, dirname, extname, relative } from 'node:path'
import fg from 'fast-glob'
import fs from 'fs-extra'
const exportSubmodules = '/* @export-submodules */'
const files = await fg('packages/**/index.ts', {
ignore: [
'**/node_modules/**',
],
absolute: true,
})
for (const file of files) {
let content = await fs.readFile(file, 'utf-8')
const index = content.indexOf(exportSubmodules)
if (index !== -1) {
const submodules = await fg(['**/*.ts'], { cwd: dirname(file), absolute: true })
const imports = submodules
.filter(i => i !== file)
.map(i => relative(dirname(file), i))
.map(i => `export * from './${basename(i, extname(i))}'`).join('\n')
content = `${content.slice(0, index) + exportSubmodules}\n${imports}\n`
await fs.writeFile(file, content, 'utf-8')
}
}

15
test/setup.ts Normal file
View File

@@ -0,0 +1,15 @@
import { afterAll, afterEach, beforeAll } from 'vitest'
import { setupServer } from 'msw/node'
import { rest } from 'msw'
export const restHandlers = [
rest.get(/google/, (req, res, ctx) => {
return res(ctx.status(200), ctx.text('@font-face mocked {}'))
}),
]
const server = setupServer(...restHandlers)
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterAll(() => server.close())
afterEach(() => server.resetHandlers())

34
tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "es2018",
"module": "esnext",
"lib": ["esnext"],
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"skipDefaultLibCheck": true,
"skipLibCheck": true,
"jsx": "preserve",
"types": [
"node",
"vite/client"
],
"paths": {
"@nicepkg/gpt-runner": ["./packages/gpt-runner/src/index.ts"],
"@nicepkg/gpt-runner-cli": ["./packages/gpt-runner-cli/src/index.ts"],
"@nicepkg/gpt-runner-config": ["./packages/gpt-runner-config/src/index.ts"],
"@nicepkg/gpt-runner-core": ["./packages/gpt-runner-core/src/index.ts"],
"@nicepkg/gpt-runner-shared": ["./packages/gpt-runner-shared/src/index.ts"],
}
},
"exclude": [
"**/dist/**",
"**/node_modules/**",
"**/playground/**",
"**/examples/**",
"**/fixtures/**",
"**/test/dts/**"
]
}

14
vitest.config.ts Normal file
View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vitest/config'
import { alias } from './alias'
export default defineConfig({
optimizeDeps: {
entries: [],
},
resolve: {
alias,
},
test: {
setupFiles: ['./test/setup.ts'],
},
})