vite-plugin-qingkuai 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "tabWidth": 4,
3
+ "semi": false,
4
+ "printWidth": 120,
5
+ "arrowParens": "avoid",
6
+ "trailingComma": "none"
7
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "vite-plugin-qingkuai",
3
+ "author": "mlgq",
4
+ "license": "MIT",
5
+ "version": "1.0.0",
6
+ "type": "module",
7
+ "keywords": [
8
+ "vite",
9
+ "qingkuai",
10
+ "vite plugin",
11
+ "vite-plugin"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "import": "./dist/index.js",
16
+ "require": "./dist/index.cjs"
17
+ }
18
+ },
19
+ "scripts": {
20
+ "dev": "rollup -c -w",
21
+ "build": "rimraf dist && rollup -c"
22
+ },
23
+ "devDependencies": {
24
+ "@types/fs-extra": "^11.0.4",
25
+ "@types/node": "^22.13.14",
26
+ "fs-extra": "^11.3.0",
27
+ "qingkuai": "^1.0.24",
28
+ "rimraf": "^6.0.1",
29
+ "rollup": "^4.38.0",
30
+ "rollup-plugin-esbuild": "^6.2.1",
31
+ "vite": "^6.2.4"
32
+ },
33
+ "peerDependencies": {
34
+ "qingkuai": "^1.0.24",
35
+ "vite": "^6.2.4"
36
+ },
37
+ "dependencies": {
38
+ "@jridgewell/sourcemap-codec": "^1.5.0",
39
+ "postcss": "^8.5.3",
40
+ "postcss-selector-parser": "^7.1.0",
41
+ "source-map-js": "^1.2.1"
42
+ }
43
+ }
@@ -0,0 +1,41 @@
1
+ import * as rollup from "rollup"
2
+ import esbuild from "rollup-plugin-esbuild"
3
+
4
+ export default rollup.defineConfig(() => {
5
+ const inputOptions = {
6
+ external: [
7
+ "vite",
8
+ "postcss",
9
+ "node:path",
10
+ "merge-source-map",
11
+ "qingkuai/compiler",
12
+ "@ampproject/remapping",
13
+ "postcss-selector-parser",
14
+ "@jridgewell/sourcemap-codec"
15
+ ],
16
+ input: {
17
+ index: "./src/index.ts"
18
+ },
19
+ output: {
20
+ dir: "dist",
21
+ format: "es",
22
+ sourcemap: true
23
+ },
24
+ plugins: [
25
+ esbuild({
26
+ target: "esNext"
27
+ })
28
+ ]
29
+ }
30
+
31
+ return [
32
+ inputOptions,
33
+ Object.assign({}, inputOptions, {
34
+ output: {
35
+ dir: "dist",
36
+ format: "cjs",
37
+ entryFileNames: "[name].cjs"
38
+ }
39
+ })
40
+ ]
41
+ })
@@ -0,0 +1,24 @@
1
+ import type { SourceMap } from "./types"
2
+
3
+ import fs from "fs-extra"
4
+ import path from "node:path"
5
+
6
+ export function generateSourceMapFiles(generate: string, map: SourceMap) {
7
+ const sourcemapPath = path.resolve(__dirname, "../sourcemap-test")
8
+ const gpath = path.resolve(sourcemapPath, "./g.css")
9
+ const mpath = path.resolve(sourcemapPath, "./s.css.map")
10
+ fs.removeSync(gpath)
11
+ fs.removeSync(mpath)
12
+ fs.writeFileSync(gpath, generate, "utf-8")
13
+ fs.writeFileSync(
14
+ mpath,
15
+ JSON.stringify({
16
+ version: 3,
17
+ names: map.names,
18
+ sources: map.sources,
19
+ mappings: map.mappings,
20
+ sourcesContent: map.sourcesContent
21
+ }),
22
+ "utf-8"
23
+ )
24
+ }
package/src/index.ts ADDED
@@ -0,0 +1,256 @@
1
+ import type { CompileResult } from "qingkuai/compiler"
2
+ import type { QingkuaiConfiguration, SourceMap } from "./types"
3
+ import type { Plugin, ResolvedConfig, ViteDevServer } from "vite"
4
+
5
+ import { findFilesByName } from "./util"
6
+ import { randomBytes } from "node:crypto"
7
+ import { offsetSourceMap } from "./sourcemap"
8
+ import { compile, util } from "qingkuai/compiler"
9
+ import { existsSync, readFileSync } from "node:fs"
10
+ import { encode } from "@jridgewell/sourcemap-codec"
11
+ import { basename, dirname, extname } from "node:path"
12
+ import { attachScopeForStyleSelectors } from "./scope"
13
+ import { transformWithEsbuild, preprocessCSS } from "vite"
14
+
15
+ export default function qingkuaiPlugin(): Plugin {
16
+ let isDev: boolean
17
+ let sourcemap: boolean
18
+ let cssSourcemap: boolean
19
+ let viteConfig: ResolvedConfig
20
+
21
+ const parseFailedConfigFiles: string[] = []
22
+ const compileResultCache = new Map<string, CompileResult>()
23
+ const qingkuaiConfigurations = new Map<string, QingkuaiConfiguration>()
24
+ const styleIdRE = /^virtual:\[\d+\].*?\.qk.(?:css|s[ac]ss|less|stylus|postcss)\?\d{13}$/
25
+
26
+ return {
27
+ name: "qingkuai-compiler",
28
+
29
+ config(_, env) {
30
+ isDev = env.command === "serve"
31
+ },
32
+
33
+ configureServer(server) {
34
+ createQingkuaiConfigurationWatcher(server)
35
+ },
36
+
37
+ configResolved(config) {
38
+ viteConfig = config
39
+ if (isDev) {
40
+ sourcemap = true
41
+ cssSourcemap = !!config.css.devSourcemap
42
+ } else {
43
+ cssSourcemap = true
44
+ sourcemap = config.build.sourcemap !== false
45
+ }
46
+ loadAllQingkuaiConfigurations(config.root)
47
+ },
48
+
49
+ resolveId(id, importer) {
50
+ if (styleIdRE.test(id)) {
51
+ return id
52
+ }
53
+ if (importer?.endsWith(".qk") && !extname(id)) {
54
+ const qingkuaiConfig = getQingkuaiConfiguration(id)
55
+ return id + (qingkuaiConfig.resolveImportExtension ? ".qk" : "")
56
+ }
57
+ },
58
+
59
+ async load(id) {
60
+ if (styleIdRE.test(id)) {
61
+ const { fileId, index } = parseStyleId(id)
62
+ if (index === -1) {
63
+ return ""
64
+ }
65
+
66
+ let virtualFileName: string
67
+ const compileRes = compileResultCache.get(fileId)!
68
+ const style = compileRes.inputDescriptor.style[index]
69
+
70
+ // create a relative and not existing file name
71
+ while (true) {
72
+ const hash = randomBytes(6).toString("hex")
73
+ virtualFileName = `${fileId}.${hash}.${style.lang}`
74
+ if (!existsSync(virtualFileName)) {
75
+ break
76
+ }
77
+ }
78
+
79
+ const preprocessRes = await preprocessCSS(style.code, virtualFileName, viteConfig)
80
+ if (!cssSourcemap) {
81
+ return preprocessRes.code
82
+ }
83
+
84
+ const assertedPreprocessMap = preprocessRes.map as SourceMap
85
+ const attachScopeResult = await attachScopeForStyleSelectors(
86
+ preprocessRes.code,
87
+ compileRes.hashId,
88
+ assertedPreprocessMap,
89
+ virtualFileName
90
+ )
91
+ const offsetMappings = encode(
92
+ offsetSourceMap(
93
+ attachScopeResult.mappings,
94
+ preprocessRes.deps?.size || 0,
95
+ style.loc.start.line - 1,
96
+ style.loc.start.column - 1
97
+ )
98
+ )
99
+ return {
100
+ code: attachScopeResult.code,
101
+ map: {
102
+ version: 3,
103
+ mappings: offsetMappings,
104
+ names: assertedPreprocessMap.names,
105
+ sources: [...assertedPreprocessMap.sources.slice(0, -1), fileId]
106
+ }
107
+ }
108
+ }
109
+ },
110
+
111
+ async transform(src, id) {
112
+ if (!id.endsWith(".qk")) {
113
+ return
114
+ }
115
+
116
+ const qingkuaiConfig = getQingkuaiConfiguration(id)
117
+ const compileRes = compile(src, {
118
+ sourcemap,
119
+ debug: isDev,
120
+ hashId: compileResultCache.get(id)?.hashId || undefined,
121
+ reserveTemplateComment: getReserveHtmlComments(qingkuaiConfig),
122
+ componentName: util.kebab2Camel(basename(id, extname(id)), true)
123
+ })
124
+ compileRes.messages.forEach(({ type, value: warning }) => {
125
+ if (type === "warning") {
126
+ this.warn(warning.message)
127
+ }
128
+ })
129
+ compileResultCache.set(id, compileRes)
130
+
131
+ const compiledCodeArr = [compileRes.code]
132
+ compileRes.inputDescriptor.style.forEach((_, index) => {
133
+ compiledCodeArr.push(`import "virtual:[${index}]${id}.css?${Date.now()}"`)
134
+ })
135
+
136
+ const baseMap: any = {
137
+ version: 3,
138
+ sources: [id],
139
+ sourcesContent: [src]
140
+ }
141
+ const compiledCode = compiledCodeArr.join("\n")
142
+ if (!compileRes.inputDescriptor.script.isTS) {
143
+ if (!sourcemap) {
144
+ return compiledCode
145
+ }
146
+ return {
147
+ code: compiledCode,
148
+ map: Object.assign(baseMap, {
149
+ mappings: compileRes.mappings
150
+ })
151
+ }
152
+ }
153
+
154
+ const esBuildCompileRes = await transformWithEsbuild(
155
+ compiledCode,
156
+ id,
157
+ {
158
+ sourcemap,
159
+ loader: "ts",
160
+ target: "esnext"
161
+ },
162
+ sourcemap
163
+ ? {
164
+ version: 3,
165
+ sources: [id],
166
+ sourcesContent: [src],
167
+ mappings: compileRes.mappings
168
+ }
169
+ : undefined
170
+ )
171
+
172
+ if (!sourcemap) {
173
+ return esBuildCompileRes.code
174
+ }
175
+ return {
176
+ code: esBuildCompileRes.code,
177
+ map: Object.assign(baseMap, {
178
+ mappings: esBuildCompileRes.map.mappings
179
+ })
180
+ }
181
+ }
182
+ }
183
+
184
+ function getQingkuaiConfiguration(id: string) {
185
+ let config: QingkuaiConfiguration = {
186
+ insertTipComments: true,
187
+ exposeDestruction: isDev,
188
+ exposeDependencies: isDev,
189
+ resolveImportExtension: true,
190
+ reserveHtmlComments: "development"
191
+ }
192
+ while (true) {
193
+ const dir = dirname(id)
194
+ if (dir === id) {
195
+ break
196
+ }
197
+
198
+ const got = qingkuaiConfigurations.get((id = dir))
199
+ if (got) {
200
+ config = got
201
+ break
202
+ }
203
+ }
204
+ return config
205
+ }
206
+
207
+ function getReserveHtmlComments(config: QingkuaiConfiguration) {
208
+ switch (config.reserveHtmlComments) {
209
+ case "all":
210
+ return true
211
+ case "never":
212
+ return false
213
+ case "production":
214
+ return !isDev
215
+ default:
216
+ return isDev
217
+ }
218
+ }
219
+
220
+ function createQingkuaiConfigurationWatcher(server: ViteDevServer) {
221
+ const watcher = server.watcher.add([".qingkuairc", "!**/node_modules/**"])
222
+ watcher.on("unlink", path => qingkuaiConfigurations.delete(dirname(path)))
223
+ watcher.on("change", recordQingkuaiConfiguration)
224
+ watcher.on("add", recordQingkuaiConfiguration)
225
+ }
226
+
227
+ function loadAllQingkuaiConfigurations(workspaceDir: string) {
228
+ findFilesByName(workspaceDir, ".qingkuairc", new Set(["node_modules"])).forEach(fileName => {
229
+ recordQingkuaiConfiguration(fileName)
230
+ })
231
+ }
232
+
233
+ function recordQingkuaiConfiguration(fileName: string) {
234
+ try {
235
+ qingkuaiConfigurations.set(dirname(fileName), JSON.parse(readFileSync(fileName, "utf-8")))
236
+ } catch {
237
+ parseFailedConfigFiles.push(fileName)
238
+ }
239
+ }
240
+ }
241
+
242
+ function parseStyleId(id: string) {
243
+ const m1 = /^virtual:\[(\d+)\]/.exec(id)!
244
+ const m2 = /\.[a-z]+\?\d{13}$/.exec(id)!
245
+ if (!m1 || !m2) {
246
+ return {
247
+ index: -1,
248
+ fileId: ""
249
+ }
250
+ }
251
+
252
+ return {
253
+ index: parseInt(m1[1]),
254
+ fileId: id.slice(m1.index + m1[0].length, m2.index)
255
+ }
256
+ }
package/src/scope.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { SourceMap } from "./types"
2
+
3
+ import postcss from "postcss"
4
+ import { getSourceFile } from "./sourcemap"
5
+ import selectorParser from "postcss-selector-parser"
6
+
7
+ export async function attachScopeForStyleSelectors(code: string, hash: string, map: SourceMap, sourceFile: string) {
8
+ const processor = postcss([
9
+ {
10
+ postcssPlugin: "postcss-attach-scope-qingkuai",
11
+ Rule(rule) {
12
+ const { line, column } = rule.source!.start!
13
+ if (getSourceFile(map, line, column) !== sourceFile) {
14
+ return
15
+ }
16
+
17
+ rule.selector = selectorParser(selectors => {
18
+ selectors.each(selector => {
19
+ const index = selector.nodes.findLastIndex(({ type }) => {
20
+ return type === "class" || type === "tag" || type === "id" || type === "universal"
21
+ })
22
+ if (index !== -1) {
23
+ const lastNode = selector.nodes[index]!
24
+ lastNode.parent?.insertAfter(
25
+ lastNode,
26
+ selectorParser.attribute({
27
+ attribute: `qk-${hash}`,
28
+ value: undefined,
29
+ raws: {}
30
+ })
31
+ )
32
+ }
33
+ })
34
+ }).processSync(rule.selector)
35
+ }
36
+ }
37
+ ])
38
+
39
+ const ret = await processor.process(code, {
40
+ map: {
41
+ prev: map,
42
+ annotation: false
43
+ }
44
+ })
45
+ return { code: ret.css, mappings: ret.map.toJSON().mappings }
46
+ }
@@ -0,0 +1,38 @@
1
+ import type { SourceMap } from "./types"
2
+ import type { SourceMapMappings } from "@jridgewell/sourcemap-codec"
3
+
4
+ import { SourceMapConsumer } from "source-map-js"
5
+ import { decode } from "@jridgewell/sourcemap-codec"
6
+
7
+ export function offsetSourceMap(
8
+ mappings: string,
9
+ sourceIndex: number,
10
+ preLine: number,
11
+ preColumn: number
12
+ ): SourceMapMappings {
13
+ return decode(mappings).map(line => {
14
+ return line.map(segment => {
15
+ if (segment.length === 1 || segment[1] !== sourceIndex) {
16
+ return segment
17
+ }
18
+ if (segment[2] === preLine) {
19
+ segment[3] += preColumn
20
+ } else {
21
+ segment[2] += preLine
22
+ }
23
+ return segment
24
+ })
25
+ })
26
+ }
27
+
28
+ export function getSourceFile(sourcemap: SourceMap, line: number, column: number) {
29
+ const consumer = new SourceMapConsumer({
30
+ ...sourcemap,
31
+ version: sourcemap.version.toString()
32
+ })
33
+
34
+ return consumer.originalPositionFor({
35
+ line: line,
36
+ column: column
37
+ }).source
38
+ }
package/src/types.ts ADDED
@@ -0,0 +1,19 @@
1
+ export interface SourceMap {
2
+ version: number
3
+ file?: string
4
+ names: string[]
5
+ mappings: string
6
+ sources: string[]
7
+ sourceRoot?: string
8
+ sourcesContent?: string[]
9
+ x_google_ignoreList?: number[]
10
+ }
11
+
12
+ export type QingkuaiConfiguration = Partial<{
13
+ exposeDestruction: boolean
14
+ exposeDependencies: boolean
15
+ insertTipComments: boolean
16
+ resolveImportExtension: boolean
17
+ convenientDerivedDeclaration: boolean
18
+ reserveHtmlComments: "all" | "never" | "development" | "production"
19
+ }>
package/src/util.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { join } from "node:path"
2
+ import { readdirSync } from "node:fs"
3
+
4
+ export function findFilesByName(dir: string, targetFileName: string, ignoreList: Set<string>) {
5
+ const result: string[] = []
6
+ const entries = readdirSync(dir, { withFileTypes: true })
7
+
8
+ for (const entry of entries) {
9
+ if (ignoreList.has(entry.name)) {
10
+ continue
11
+ }
12
+
13
+ const fullPath = join(dir, entry.name)
14
+ if (entry.name === targetFileName) {
15
+ result.push(fullPath)
16
+ } else if (entry.isDirectory()) {
17
+ result.push(...findFilesByName(fullPath, targetFileName, ignoreList))
18
+ }
19
+ }
20
+ return result
21
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "strict": true,
4
+ "noEmit": true,
5
+ "target": "ESNext",
6
+ "skipLibCheck": true,
7
+ "moduleResolution": "bundler"
8
+ }
9
+ }