@liangmi/vp-config 0.0.0 → 0.1.1

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/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # @liangmi/vp-config
2
2
 
3
- Liang's opinionated Vite+ config presets for JavaScript development, including linting, formatting, task running and more.
4
-
5
- It is designed to work with different kinds of project, cli / tui development, library, and website development (WIP, waiting for [Oxlint's better Vue support](https://github.com/oxc-project/oxc/issues/15761))
3
+ Liang's united [Vite+](https://viteplus.dev/) config presets for JavaScript development opinionated, strict and designed to be universal with different kinds of projects.
6
4
 
7
5
  ## Usage
8
6
 
@@ -15,15 +13,116 @@ vp install -D @liangmi/vp-config
15
13
  And modify your `vite.config.ts` like that:
16
14
 
17
15
  ```typescript
18
- import { cli } from "@liangmi/vp-config";
16
+ import { base } from "@liangmi/vp-config";
17
+
18
+ export default base({
19
+ /* Your personal config overrides, will be merged deeply */
20
+ });
21
+ ```
22
+
23
+ > [!WARNING]
24
+ > Considering Vite+ is now still alpha, the internal API can be unstable and expected to change, please manage to use `@liangmi/vp-config` with the latest Vite+. If you found something that doesn't work expectedly, please [submit an issue](https://github.com/liangmiQwQ/vp-config/issues/new).
25
+
26
+ ### Categories
27
+
28
+ We provide four config categories for different kinds of projects.
29
+
30
+ | Category | Description | Recommended for |
31
+ | --------- | ------------------------------------------------------ | -------------------- |
32
+ | `base` | Pure and basic config | Workspace root |
33
+ | `cli` | Config for Node.js CLI and React/Vue Tui applications | CLI and TUI projects |
34
+ | `lib` | Config with library bundling defaults | Libraries |
35
+ | `website` | Config for browser environment and website development | Websites |
36
+
37
+ The `website` category is experimental and its defaults may change before the package reaches a stable release. React and browser linting are available, but Vue template linting is still waiting for [better Vue support in Oxlint](https://github.com/oxc-project/oxc/issues/15761).
38
+
39
+ For monorepos, different presets should be used in combination. We should use `base` the workspace root, and use other categories as needed.
40
+
41
+ ### Customizable
42
+
43
+ Each category is a wrapper of Vite+'s `defineConfig`. The config passed to it overrides and deeply merges with the preset.
44
+
45
+ > [!TIP]
46
+ >
47
+ > Deep merging means your config only needs to specify what should change. Nested preset options that you do not override remain enabled, while values from your config take precedence.
48
+
49
+ Use `.only()` to load only selected parts of a preset:
50
+
51
+ ```typescript
52
+ import { base } from "@liangmi/vp-config";
19
53
 
20
- export default cli({
21
- /* Your personal config overrides */
54
+ export default base.only(["lint", "fmt"], {
55
+ lint: {
56
+ /* Your own lint config */
57
+ },
58
+ fmt: {
59
+ /* Your own format config */
60
+ },
22
61
  });
23
62
  ```
24
63
 
25
- Considering Vite+ is now still alpha, the internal API can be unstable and expected to change, please manage to use `@liangmi/vp-config` with the latest Vite+. If you found something that doesn't work expectedly, please [submit an issue](https://github.com/liangmiQwQ/vp-config/issues/new).
64
+ Use `.exclude()` to omit selected parts while keeping the rest:
65
+
66
+ ```typescript
67
+ import { base } from "@liangmi/vp-config";
68
+
69
+ export default base.exclude(["staged"], {});
70
+ ```
71
+
72
+ Available config parts are `fmt`, `lint`, `pack`, `run`, and `staged`, depending on the selected category.
73
+
74
+ ## What's included by default
75
+
76
+ Vite+ is a united toolchain for JavaScript development, it includes linting, formatting, library bundling, git hooks, test, task runner, website development, etc.
77
+
78
+ `@liangmi/vp-config` tries to extract reusable configurations from these, in order to improve the development experience as much as possible.
79
+
80
+ ### Lint
81
+
82
+ The lint config prioritizes correctness and fast feedback. Rules that prevent bugs report errors, while style and fixable readability rules generally report warnings and are left to autofixes. Warnings also fail the lint command, keeping the codebase consistent without treating every style concern as a hand-written task.
83
+
84
+ All categories include a strict Oxlint config with type-aware linting and type checking enabled, which means you do not need to run `tsc` manually. Correctness, performance, suspicious, and nursery rules report errors.
85
+
86
+ `console.log` is not allowed except in the `cli` category and Node.js script files. Test files enable Vitest rules, while the `cli` and `website` categories add React and Vue component rules. The `website` category also enables browser-specific rules.
87
+
88
+ The included `liangmi` Oxlint plugin checks that presets are loaded correctly. It reports orphan `vite.config.ts` files, preset imports outside config files, missing preset wrappers, improper root or project categories, and configs that mix library and website signals.
89
+
90
+ ### Format
91
+
92
+ The format config follows a simple philosophy: remove syntax that does not improve readability, and let the formatter handle mechanical consistency.
93
+
94
+ All categories include an Oxfmt config using single quotes, no semicolons, no unnecessary trailing commas, sorted imports, and sorted `package.json` fields. Embedded-language formatting is disabled for `base` and `lib`, and enabled for `cli` and `website`.
95
+
96
+ ### Pack
97
+
98
+ The packaging presets provide the best-practice defaults for each kind of project while leaving project-specific details explicit. Because the package entry depends on the project's source layout, you still need to define `pack.entry` manually.
99
+
100
+ The `lib` preset generates `.d.ts` and package exports with fixed extensions. The `cli` preset targets Node.js, minifies output, strips `node:` protocol prefixes, and disables `dts` generation.
101
+
102
+ ### Cached commands
103
+
104
+ In order to make full use of Vite+'s powerful cache system without too much config and make it contributors-friendly, we provide cached tasks task wrappers for common Vite+ commands. This feature is included in all categories.
105
+
106
+ In most cases, they should be treated more like cached versions of Vite+ commands rather than normal user-defined tasks. For example, users can run `vpr ccheck` as a cached replacement for `vp check`.
107
+
108
+ | Task | Command |
109
+ | --------- | ----------- |
110
+ | `cbuild` | `vp build` |
111
+ | `cpack` | `vp pack` |
112
+ | `clint` | `vp lint` |
113
+ | `cfmt` | `vp fmt` |
114
+ | `cformat` | `vp format` |
115
+ | `ccheck` | `vp check` |
116
+ | `ctest` | `vp test` |
117
+
118
+ Run them with `vp run <task>`, such as `vp run cpack`, or shorthand `vpr cpack`. They can also be used in `package.json` scripts while retaining Vite+ task caching.
119
+
120
+ ### Staged files
121
+
122
+ Staged-file checks keep automatic fixes close to the commit workflow, so only code that is about to be committed is processed.
123
+
124
+ All categories run `vp check --fix` for staged files. Run `vp config` to install Vite+'s commit hook.
26
125
 
27
126
  ## License
28
127
 
29
- [MIT](./LICENSE) License © 2026-PRESENT Liang
128
+ [MIT](./LICENSE) License © 2026-PRESENT Liang & Contributors
@@ -0,0 +1,5 @@
1
+ //#region src/oxlint-plugin/constants.d.ts
2
+ declare const configNames: readonly ["base", "cli", "lib", "website"];
3
+ type ConfigName = (typeof configNames)[number];
4
+ //#endregion
5
+ export { ConfigName as t };
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { c as writeRuntimeInfo } from "./info-CHtwxjhy.mjs";
1
2
  import { defineConfig, mergeConfig } from "vite-plus";
2
3
  //#region src/base/fmt.ts
3
4
  const fmtBase = {
@@ -53,7 +54,10 @@ const componentOverride = {
53
54
  };
54
55
  //#endregion
55
56
  //#region src/base/lint.ts
56
- const suspicious = { "no-shadow": "off" };
57
+ const suspicious = {
58
+ "no-shadow": "off",
59
+ "typescript/no-unsafe-type-assertion": "off"
60
+ };
57
61
  const nursery = {
58
62
  "no-undef": "off",
59
63
  "import/named": "off",
@@ -64,7 +68,7 @@ const style = {
64
68
  "typescript/consistent-type-definitions": ["warn", "interface"],
65
69
  "typescript/consistent-indexed-object-style": ["warn", "record"],
66
70
  "eslint/func-names": ["warn", "never"],
67
- "id-length": ["warn", { checkGeneric: false }],
71
+ "typescript/method-signature-style": ["warn", "property"],
68
72
  "unicorn/prefer-ternary": ["warn", "only-single-line"],
69
73
  "import/exports-last": "off",
70
74
  "func-style": "off",
@@ -86,7 +90,10 @@ const style = {
86
90
  "import/no-nodejs-modules": "off",
87
91
  "import/prefer-default-export": "off",
88
92
  "prefer-await-to-then": "off",
89
- "unicorn/no-null": "off"
93
+ "unicorn/no-null": "off",
94
+ "prefer-named-capture-group": "off",
95
+ "id-length": "off",
96
+ "promise/avoid-new": "off"
90
97
  };
91
98
  const restriction = {
92
99
  "oxc/bad-bitwise-operator": "error",
@@ -211,6 +218,10 @@ const lintBase = {
211
218
  restriction: "off",
212
219
  pedantic: "off"
213
220
  },
221
+ jsPlugins: [{
222
+ name: "liangmi",
223
+ specifier: "@liangmi/vp-config/oxlint-plugin"
224
+ }],
214
225
  plugins: [
215
226
  "eslint",
216
227
  "oxc",
@@ -225,7 +236,11 @@ const lintBase = {
225
236
  ...restriction,
226
237
  ...pedantic,
227
238
  ...style,
228
- "typescript/no-unsafe-type-assertion": "off"
239
+ "liangmi/no-orphan-vite-config": "error",
240
+ "liangmi/no-useless-vp-preset-imports": "error",
241
+ "liangmi/use-preset-vp-config": "error",
242
+ "liangmi/load-proper-vp-config-category": "error",
243
+ "liangmi/no-mixed-project": "error"
229
244
  },
230
245
  options: {
231
246
  typeAware: true,
@@ -240,6 +255,24 @@ const lintBase = {
240
255
  node: true,
241
256
  vitest: true
242
257
  },
258
+ plugins: ["vitest"],
259
+ rules: {
260
+ "vitest/consistent-test-filename": ["warn", { pattern: ".*.test.ts$" }],
261
+ "vitest/consistent-test-it": ["warn", {
262
+ fn: "it",
263
+ withinDescribe: "it"
264
+ }],
265
+ "vitest/no-hooks": "off",
266
+ "vitest/require-top-level-describe": "off",
267
+ "vitest/prefer-strict-boolean-matchers": "off",
268
+ "vitest/max-expects": "off",
269
+ "vitest/prefer-expect-assertions": "off",
270
+ "vitest/prefer-importing-vitest-globals": "off",
271
+ "vitest/no-importing-vitest-globals": "off",
272
+ "vitest/no-large-snapshots": "off",
273
+ "vitest/no-restricted-matchers": "off",
274
+ "vitest/no-restricted-vi-methods": "off"
275
+ },
243
276
  files: ["*.test.ts", "*.spec.ts"]
244
277
  },
245
278
  {
@@ -261,42 +294,79 @@ const lintBase = {
261
294
  ]
262
295
  };
263
296
  //#endregion
297
+ //#region src/base/run.ts
298
+ const lintInput = ["!node_modules/.vp-config/info.json", "index.html"];
299
+ const runBase = {
300
+ tasks: {
301
+ cbuild: "vp build",
302
+ ccheck: {
303
+ command: "vp check",
304
+ input: lintInput
305
+ },
306
+ cfmt: "vp fmt",
307
+ cformat: "vp format",
308
+ clint: {
309
+ command: "vp lint",
310
+ input: lintInput
311
+ },
312
+ cpack: "vp pack",
313
+ ctest: "vp test"
314
+ },
315
+ cache: {
316
+ scripts: false,
317
+ tasks: true
318
+ }
319
+ };
320
+ //#endregion
321
+ //#region src/base/staged.ts
322
+ const stagedBase = { "*": "vp check --fix" };
323
+ //#endregion
264
324
  //#region src/base/index.ts
265
325
  const baseConfig = {
266
326
  fmt: fmtBase,
267
- lint: lintBase
327
+ lint: lintBase,
328
+ run: runBase,
329
+ staged: stagedBase
268
330
  };
269
331
  //#endregion
270
332
  //#region src/website/fmt.ts
271
333
  const fmtWebsite = {
334
+ ...fmtBase,
272
335
  jsxSingleQuote: true,
273
336
  embeddedLanguageFormatting: "auto",
274
- sortTailwindcss: false,
275
- ...fmtBase
337
+ sortTailwindcss: false
276
338
  };
277
339
  //#endregion
278
340
  //#region src/cli/index.ts
279
341
  const cliConfig = {
280
342
  fmt: fmtWebsite,
281
- lint: mergeConfig(lintBase, mergeConfig(lintBase, componentOverride))
343
+ lint: mergeConfig(lintBase, mergeConfig(cliOverride, componentOverride)),
344
+ pack: {
345
+ dts: false,
346
+ minify: true,
347
+ platform: "node",
348
+ nodeProtocol: "strip"
349
+ },
350
+ run: runBase,
351
+ staged: stagedBase
282
352
  };
283
353
  //#endregion
284
354
  //#region src/entry.ts
285
- function createConfigEntry(presetConfig) {
286
- const entry = ((config) => defineMergedConfig(presetConfig, config));
287
- const only = (parts, ...args) => defineMergedConfig(pickPresetConfig(presetConfig, parts), ...args);
288
- const exclude = (parts, ...args) => defineMergedConfig(omitPresetConfig(presetConfig, parts), ...args);
355
+ function createConfigEntry(presetConfig, category) {
356
+ const entry = ((config) => defineMergedConfig(presetConfig, config, category));
357
+ const only = (parts, ...args) => defineMergedConfig(pickPresetConfig(presetConfig, parts), args[0], category);
358
+ const exclude = (parts, ...args) => defineMergedConfig(omitPresetConfig(presetConfig, parts), args[0], category);
289
359
  return Object.assign(entry, {
290
360
  only,
291
361
  exclude
292
362
  });
293
363
  }
294
- function defineMergedConfig(presetConfig, config) {
364
+ function defineMergedConfig(presetConfig, config, category) {
295
365
  if (typeof config === "function") return defineConfig(async (env) => {
296
- return mergePresetConfig(presetConfig, await config(env));
366
+ return trackRuntimeInfo(mergePresetConfig(presetConfig, await config(env)), category);
297
367
  });
298
- if (config instanceof Promise) return defineConfig(config.then((userConfig) => mergePresetConfig(presetConfig, userConfig)));
299
- return defineConfig(mergePresetConfig(presetConfig, config));
368
+ if (config instanceof Promise) return defineConfig(config.then((userConfig) => trackRuntimeInfo(mergePresetConfig(presetConfig, userConfig), category)));
369
+ return defineConfig(trackRuntimeInfo(mergePresetConfig(presetConfig, config), category));
300
370
  }
301
371
  function pickPresetConfig(presetConfig, parts) {
302
372
  return Object.fromEntries(parts.map((part) => [part, presetConfig[part]]));
@@ -314,6 +384,13 @@ function mergePresetConfig(presetConfig, userConfig) {
314
384
  if (presetConfig.staged) config.staged = mergeStagedConfig(presetConfig.staged, userConfig.staged);
315
385
  return config;
316
386
  }
387
+ function trackRuntimeInfo(config, category) {
388
+ writeRuntimeInfo({
389
+ category,
390
+ config
391
+ });
392
+ return config;
393
+ }
317
394
  function mergeLintConfig(presetLint, userLint) {
318
395
  return mergeConfig(presetLint, userLint ?? {});
319
396
  }
@@ -333,7 +410,14 @@ function isStagedObjectConfig(config) {
333
410
  //#region src/lib/index.ts
334
411
  const libConfig = {
335
412
  fmt: fmtBase,
336
- lint: mergeConfig(lintBase, {})
413
+ lint: mergeConfig(lintBase, {}),
414
+ pack: {
415
+ fixedExtension: true,
416
+ exports: true,
417
+ dts: { tsgo: true }
418
+ },
419
+ run: runBase,
420
+ staged: stagedBase
337
421
  };
338
422
  //#endregion
339
423
  //#region src/website/index.ts
@@ -349,13 +433,15 @@ const websiteConfig = {
349
433
  "prefer-blob-reading-methods": "warn",
350
434
  "import/no-unassigned-import": ["error", { allow: ["**/*.css"] }]
351
435
  }
352
- })
436
+ }),
437
+ run: runBase,
438
+ staged: stagedBase
353
439
  };
354
440
  //#endregion
355
441
  //#region src/index.ts
356
- const base = createConfigEntry(baseConfig);
357
- const cli = createConfigEntry(cliConfig);
358
- const lib = createConfigEntry(libConfig);
359
- const website = createConfigEntry(websiteConfig);
442
+ const base = createConfigEntry(baseConfig, "base");
443
+ const cli = createConfigEntry(cliConfig, "cli");
444
+ const lib = createConfigEntry(libConfig, "lib");
445
+ const website = createConfigEntry(websiteConfig, "website");
360
446
  //#endregion
361
447
  export { base, cli, lib, website };
@@ -0,0 +1,306 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, normalize } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ //#region src/oxlint-plugin/constants.ts
5
+ const packageName = "@liangmi/vp-config";
6
+ const pluginName = "liangmi";
7
+ const infoDirectoryName = ".vp-config";
8
+ const infoFileName = "info.json";
9
+ const projectConfigNames = [
10
+ "cli",
11
+ "lib",
12
+ "website"
13
+ ];
14
+ const rootConfigNames = ["base"];
15
+ const configNames = [...rootConfigNames, ...projectConfigNames];
16
+ const viteConfigNames = [
17
+ "vite.config.ts",
18
+ "vite.config.mts",
19
+ "vite.config.cts",
20
+ "vite.config.js",
21
+ "vite.config.mjs",
22
+ "vite.config.cjs"
23
+ ];
24
+ function isVpConfigEntrySpecifier(specifier) {
25
+ return specifier === "@liangmi/vp-config" || /(?:^|[\\/])src[\\/]index\.ts$/u.test(specifier) || specifier.endsWith("/src/index.ts");
26
+ }
27
+ //#endregion
28
+ //#region src/oxlint-plugin/info.ts
29
+ function getInfoPath(configDirectory) {
30
+ return join(configDirectory, "node_modules", infoDirectoryName, infoFileName);
31
+ }
32
+ function readRuntimeInfo(configDirectory) {
33
+ const infoPath = getInfoPath(configDirectory);
34
+ if (!existsSync(infoPath)) return;
35
+ try {
36
+ const info = JSON.parse(readFileSync(infoPath, "utf8"));
37
+ return isRuntimeInfo(info) ? info : void 0;
38
+ } catch {
39
+ return;
40
+ }
41
+ }
42
+ function writeRuntimeInfo(input) {
43
+ const { stack } = /* @__PURE__ */ new Error("Find vite config caller");
44
+ if (!shouldWriteRuntimeInfo(stack)) return;
45
+ const configFile = findConfigFileFromStack(stack) ?? findCwdConfigFile();
46
+ if (!configFile) return;
47
+ writeRuntimeInfoFile(configFile, input);
48
+ }
49
+ function ensureRuntimeInfo(configFile) {
50
+ const configDirectory = dirname(configFile);
51
+ if (!hasConfigPackageJson(configFile)) return;
52
+ loadRuntimeInfo(configFile);
53
+ return readRuntimeInfo(configDirectory);
54
+ }
55
+ function cleanupRuntimeInfo(configDirectory) {
56
+ const info = readRuntimeInfo(configDirectory);
57
+ const nodeModulesDirectory = join(configDirectory, "node_modules");
58
+ rmSync(getInfoPath(configDirectory), { force: true });
59
+ rmSync(join(nodeModulesDirectory, infoDirectoryName), {
60
+ recursive: true,
61
+ force: true
62
+ });
63
+ if (info?.cleanup.createdNodeModules) rmSync(nodeModulesDirectory, {
64
+ recursive: true,
65
+ force: true
66
+ });
67
+ }
68
+ function writeRuntimeInfoFile(configFile, input) {
69
+ if (!hasConfigPackageJson(configFile)) return;
70
+ const configDirectory = dirname(configFile);
71
+ const createdNodeModules = !existsSync(join(configDirectory, "node_modules"));
72
+ const infoPath = getInfoPath(configDirectory);
73
+ rmSync(infoPath, { force: true });
74
+ mkdirSync(dirname(infoPath), { recursive: true });
75
+ writeFileSync(infoPath, `${JSON.stringify(createRuntimeInfo(configFile, input, createdNodeModules), null, 2)}\n`);
76
+ }
77
+ function shouldWriteRuntimeInfo(stack) {
78
+ return hasOxlintEntryStack(stack) || hasVitePlusResolveStack(stack);
79
+ }
80
+ function hasOxlintEntryStack(stack) {
81
+ return Boolean(stack && /node_modules[\\/](?:\.pnpm[\\/]oxlint@[^\\/]+[\\/]node_modules[\\/])?oxlint[\\/]dist[\\/]/u.test(stack));
82
+ }
83
+ function hasVitePlusResolveStack(stack) {
84
+ return Boolean(stack && /node_modules[\\/]vite-plus[\\/]dist[\\/]resolve-vite-config(?:-[^\\/]+)?\.js/u.test(stack));
85
+ }
86
+ function loadRuntimeInfo(configFile) {
87
+ const input = readRuntimeConfigInput(configFile);
88
+ if (input) writeRuntimeInfoFile(configFile, input);
89
+ }
90
+ function createRuntimeInfo(configFile, input, createdNodeModules) {
91
+ const configDirectory = dirname(configFile);
92
+ const configKeys = Object.keys(input.config);
93
+ const hasPack = configKeys.includes("pack");
94
+ const hasViteConfigFields = configKeys.some(isViteConfigField);
95
+ return {
96
+ version: 1,
97
+ configFile,
98
+ configDirectory,
99
+ category: input.category,
100
+ configKeys,
101
+ project: {
102
+ hasViteConfigFields,
103
+ hasPack
104
+ },
105
+ cleanup: { createdNodeModules }
106
+ };
107
+ }
108
+ const vitePlusConfigKeys = new Set([
109
+ "create",
110
+ "fmt",
111
+ "lint",
112
+ "pack",
113
+ "run",
114
+ "staged",
115
+ "test"
116
+ ]);
117
+ function isViteConfigField(key) {
118
+ return !vitePlusConfigKeys.has(key);
119
+ }
120
+ function findConfigFileFromStack(stack) {
121
+ if (!stack) return;
122
+ for (const rawLine of stack.split("\n")) {
123
+ const path = normalizeStackPath(rawLine);
124
+ const configPath = path ? resolveBundledConfigPath(path) : void 0;
125
+ if (configPath && viteConfigNames.includes(getPathBasename(configPath))) return configPath;
126
+ }
127
+ }
128
+ function getPathBasename(path) {
129
+ return path.split(/[\\/]/u).at(-1) ?? path;
130
+ }
131
+ function normalizeStackPath(line) {
132
+ const fileUrlMatch = /file:\/\/\/.*?vite\.config\.[cm]?[jt]s/u.exec(line);
133
+ if (fileUrlMatch) return fileURLToPath(fileUrlMatch[0]);
134
+ const absolutePathMatch = /((?:[a-zA-Z]:[\\/]|\/)[^:)]+vite\.config\.[cm]?[jt]s)/u.exec(line);
135
+ return absolutePathMatch ? normalize(absolutePathMatch[1]) : void 0;
136
+ }
137
+ function resolveBundledConfigPath(path) {
138
+ const bundledConfigMatch = /^(.*)([\\/])node_modules[\\/]\.vite-temp[\\/](vite\.config\.[cm]?[jt]s)$/u.exec(path);
139
+ return bundledConfigMatch ? `${bundledConfigMatch[1]}${bundledConfigMatch[2]}${bundledConfigMatch[3]}` : path;
140
+ }
141
+ function readRuntimeConfigInput(configFile) {
142
+ try {
143
+ return parseRuntimeConfigInput(readFileSync(configFile, "utf8"));
144
+ } catch {
145
+ return;
146
+ }
147
+ }
148
+ function parseRuntimeConfigInput(source) {
149
+ const imports = getConfigImports(source);
150
+ for (const [localName, category] of imports.named) {
151
+ const input = findConfigCallInput(source, localName, category);
152
+ if (input) return input;
153
+ }
154
+ for (const namespace of imports.namespaces) for (const category of configNames) {
155
+ const input = findConfigCallInput(source, `${namespace}.${category}`, category);
156
+ if (input) return input;
157
+ }
158
+ }
159
+ function getConfigImports(source) {
160
+ const named = /* @__PURE__ */ new Map();
161
+ const namespaces = /* @__PURE__ */ new Set();
162
+ for (const match of source.matchAll(/import\s*\{(?<imports>[^}]+)\}\s*from\s*['"](?<specifier>[^'"]+)['"]/gu)) {
163
+ if (!isConfigEntrySpecifier(match.groups?.specifier)) continue;
164
+ for (const specifier of splitSimpleList(match.groups?.imports ?? "")) {
165
+ const [importedName, localName] = specifier.split(/\s+as\s+/u).map((part) => part.trim());
166
+ const category = toConfigName(importedName);
167
+ if (category) named.set(localName || importedName, category);
168
+ }
169
+ }
170
+ for (const match of source.matchAll(/import\s*\*\s*as\s*(?<localName>[$A-Z_a-z][$\w]*)\s*from\s*['"](?<specifier>[^'"]+)['"]/gu)) if (isConfigEntrySpecifier(match.groups?.specifier) && match.groups?.localName) namespaces.add(match.groups.localName);
171
+ for (const match of source.matchAll(/const\s*\{(?<imports>[^}]+)\}\s*=\s*require\(\s*['"](?<specifier>[^'"]+)['"]\s*\)/gu)) {
172
+ if (!isConfigEntrySpecifier(match.groups?.specifier)) continue;
173
+ for (const specifier of splitSimpleList(match.groups?.imports ?? "")) {
174
+ const [importedName, localName] = specifier.split(/\s*:\s*/u).map((part) => part.trim());
175
+ const category = toConfigName(importedName);
176
+ if (category) named.set(localName || importedName, category);
177
+ }
178
+ }
179
+ return {
180
+ named,
181
+ namespaces
182
+ };
183
+ }
184
+ function findConfigCallInput(source, callee, category) {
185
+ const callPattern = new RegExp(`(^|[^$\\w])${callee.split(".").map((part) => escapeRegExp(part)).join(String.raw`\s*\.\s*`)}\\s*(?:\\.\\s*(?<method>only|exclude)\\s*)?\\(`, "gu");
186
+ for (const match of source.matchAll(callPattern)) {
187
+ const argument = readCallArgument(source, match.index + match[0].lastIndexOf("("), match.groups?.method ? 1 : 0);
188
+ const configKeys = argument ? readObjectKeys(argument) : [];
189
+ if (configKeys.length > 0 || argument?.trim() === "{}") return {
190
+ category,
191
+ config: Object.fromEntries(configKeys.map((key) => [key, true]))
192
+ };
193
+ }
194
+ }
195
+ function readCallArgument(source, openParenIndex, argumentIndex) {
196
+ const closeParenIndex = findClosingDelimiter(source, openParenIndex, "(", ")");
197
+ if (closeParenIndex === void 0) return;
198
+ return splitTopLevel(source.slice(openParenIndex + 1, closeParenIndex))[argumentIndex];
199
+ }
200
+ function readObjectKeys(source) {
201
+ const objectStart = source.search(/\S/u);
202
+ if (objectStart === -1 || source[objectStart] !== "{") return [];
203
+ const objectEnd = findClosingDelimiter(source, objectStart, "{", "}");
204
+ if (objectEnd === void 0) return [];
205
+ return splitTopLevel(source.slice(objectStart + 1, objectEnd)).flatMap(readPropertyKey);
206
+ }
207
+ function readPropertyKey(source) {
208
+ const property = source.trim();
209
+ if (!property || property.startsWith("...") || property.startsWith("[")) return [];
210
+ const quotedKeyMatch = /^['"](?<key>[^'"]+)['"]\s*[:(]/u.exec(property);
211
+ if (quotedKeyMatch?.groups?.key) return [quotedKeyMatch.groups.key];
212
+ const identifierKeyMatch = /^(?:async\s+|get\s+|set\s+)?(?<key>[$A-Z_a-z][$\w]*)\s*(?::|\(|$)/u.exec(property);
213
+ return identifierKeyMatch?.groups?.key ? [identifierKeyMatch.groups.key] : [];
214
+ }
215
+ function splitTopLevel(source) {
216
+ const parts = [];
217
+ let start = 0;
218
+ let depth = 0;
219
+ for (let index = 0; index < source.length; index += 1) {
220
+ const skippedIndex = skipSyntax(source, index);
221
+ if (skippedIndex !== index) {
222
+ index = skippedIndex;
223
+ continue;
224
+ }
225
+ const char = source[index];
226
+ if (char === "(" || char === "[" || char === "{") depth += 1;
227
+ else if (char === ")" || char === "]" || char === "}") depth -= 1;
228
+ else if (char === "," && depth === 0) {
229
+ parts.push(source.slice(start, index).trim());
230
+ start = index + 1;
231
+ }
232
+ }
233
+ parts.push(source.slice(start).trim());
234
+ return parts.filter(Boolean);
235
+ }
236
+ function findClosingDelimiter(source, openIndex, openDelimiter, closeDelimiter) {
237
+ let depth = 0;
238
+ for (let index = openIndex; index < source.length; index += 1) {
239
+ const skippedIndex = skipSyntax(source, index);
240
+ if (skippedIndex !== index) {
241
+ index = skippedIndex;
242
+ continue;
243
+ }
244
+ if (source[index] === openDelimiter) depth += 1;
245
+ else if (source[index] === closeDelimiter) {
246
+ depth -= 1;
247
+ if (depth === 0) return index;
248
+ }
249
+ }
250
+ }
251
+ function skipSyntax(source, index) {
252
+ const char = source[index];
253
+ const nextChar = source[index + 1];
254
+ if (char === "\"" || char === "'" || char === "`") return skipString(source, index, char);
255
+ if (char === "/" && nextChar === "/") return skipLineComment(source, index);
256
+ if (char === "/" && nextChar === "*") return skipBlockComment(source, index);
257
+ return index;
258
+ }
259
+ function skipString(source, start, quote) {
260
+ for (let index = start + 1; index < source.length; index += 1) if (source[index] === "\\") index += 1;
261
+ else if (source[index] === quote) return index;
262
+ return source.length - 1;
263
+ }
264
+ function skipLineComment(source, start) {
265
+ const end = source.indexOf("\n", start + 2);
266
+ return end === -1 ? source.length - 1 : end;
267
+ }
268
+ function skipBlockComment(source, start) {
269
+ const end = source.indexOf("*/", start + 2);
270
+ return end === -1 ? source.length - 1 : end + 1;
271
+ }
272
+ function splitSimpleList(source) {
273
+ return source.split(",").map((part) => part.trim()).filter(Boolean);
274
+ }
275
+ function isConfigEntrySpecifier(specifier) {
276
+ return specifier ? isVpConfigEntrySpecifier(specifier) : false;
277
+ }
278
+ function toConfigName(name) {
279
+ return configNames.includes(name) ? name : void 0;
280
+ }
281
+ function escapeRegExp(value) {
282
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
283
+ }
284
+ function findCwdConfigFile() {
285
+ for (const configName of viteConfigNames) {
286
+ const configFile = join(process.cwd(), configName);
287
+ if (existsSync(configFile)) return configFile;
288
+ }
289
+ }
290
+ function hasConfigPackageJson(configFile) {
291
+ return existsSync(join(dirname(configFile), "package.json"));
292
+ }
293
+ function isRuntimeInfo(value) {
294
+ return typeof value === "object" && value !== null && "version" in value && value.version === 1 && "project" in value && typeof value.project === "object" && value.project !== null;
295
+ }
296
+ function isWebsiteProject(info) {
297
+ return existsSync(join(info.configDirectory, "index.html")) || info.project.hasViteConfigFields;
298
+ }
299
+ function isLibProject(info) {
300
+ return info.project.hasPack;
301
+ }
302
+ function isProject(info) {
303
+ return isWebsiteProject(info) || isLibProject(info);
304
+ }
305
+ //#endregion
306
+ export { isProject as a, writeRuntimeInfo as c, projectConfigNames as d, rootConfigNames as f, isLibProject as i, packageName as l, ensureRuntimeInfo as n, isWebsiteProject as o, viteConfigNames as p, getInfoPath as r, readRuntimeInfo as s, cleanupRuntimeInfo as t, pluginName as u };
@@ -0,0 +1,34 @@
1
+ import { t as ConfigName } from "../constants-D9DskZO3.mjs";
2
+
3
+ //#region src/oxlint-plugin/project.d.ts
4
+ declare function getAllowedConfigNames(filename: string): readonly ConfigName[];
5
+ declare function isVpConfigImportAllowed(filename: string, importedNames: readonly string[]): boolean;
6
+ //#endregion
7
+ //#region src/oxlint-plugin/info.d.ts
8
+ interface VpConfigRuntimeInfo {
9
+ version: 1;
10
+ configFile: string;
11
+ configDirectory: string;
12
+ category?: ConfigName;
13
+ configKeys: string[];
14
+ project: {
15
+ hasViteConfigFields: boolean;
16
+ hasPack: boolean;
17
+ };
18
+ cleanup: {
19
+ createdNodeModules: boolean;
20
+ };
21
+ }
22
+ interface RuntimeConfigInput {
23
+ category?: ConfigName;
24
+ config: Record<string, unknown>;
25
+ }
26
+ declare function getInfoPath(configDirectory: string): string;
27
+ declare function readRuntimeInfo(configDirectory: string): VpConfigRuntimeInfo | undefined;
28
+ declare function writeRuntimeInfo(input: RuntimeConfigInput): void;
29
+ declare function cleanupRuntimeInfo(configDirectory: string): void;
30
+ //#endregion
31
+ //#region src/oxlint-plugin/index.d.ts
32
+ declare const _default: import("@oxlint/plugins").Plugin;
33
+ //#endregion
34
+ export { cleanupRuntimeInfo, _default as default, getAllowedConfigNames, getInfoPath, isVpConfigImportAllowed, readRuntimeInfo, writeRuntimeInfo };
@@ -0,0 +1,161 @@
1
+ import { a as isProject, c as writeRuntimeInfo, d as projectConfigNames, f as rootConfigNames, i as isLibProject, l as packageName, n as ensureRuntimeInfo, o as isWebsiteProject, p as viteConfigNames, r as getInfoPath, s as readRuntimeInfo, t as cleanupRuntimeInfo, u as pluginName } from "../info-CHtwxjhy.mjs";
2
+ import { existsSync } from "node:fs";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { definePlugin, defineRule } from "@oxlint/plugins";
5
+ //#region src/oxlint-plugin/ast.ts
6
+ function isVpConfigSpecifier(specifier) {
7
+ return specifier === packageName;
8
+ }
9
+ function isStringLiteral(node) {
10
+ return node.type === "Literal" && typeof node.value === "string";
11
+ }
12
+ //#endregion
13
+ //#region src/oxlint-plugin/project.ts
14
+ function getAllowedConfigNames(filename) {
15
+ if (!isViteConfigFile(filename)) return [];
16
+ const info = readRuntimeInfo(dirname(filename));
17
+ return (info ? isProject(info) : isStaticWebsiteProjectDirectory(dirname(filename))) ? projectConfigNames : rootConfigNames;
18
+ }
19
+ function isVpConfigImportAllowed(filename, importedNames) {
20
+ const allowedNames = getAllowedConfigNames(filename);
21
+ const allowedNameSet = new Set(allowedNames);
22
+ return allowedNames.length > 0 && importedNames.length > 0 && importedNames.every((name) => allowedNameSet.has(name));
23
+ }
24
+ function isViteConfigFile(filename) {
25
+ return viteConfigNames.includes(basename(filename));
26
+ }
27
+ function hasPackageJson(directory) {
28
+ return existsSync(join(directory, "package.json"));
29
+ }
30
+ function isStaticWebsiteProjectDirectory(directory) {
31
+ return existsSync(join(directory, "index.html"));
32
+ }
33
+ //#endregion
34
+ //#region src/oxlint-plugin/rules.ts
35
+ function canUseRuntimeInfo(filename) {
36
+ return isViteConfigFile(filename) && hasPackageJson(dirname(filename));
37
+ }
38
+ const rules = {
39
+ "no-orphan-vite-config": defineRule({
40
+ meta: {
41
+ type: "problem",
42
+ docs: {
43
+ description: "Require vite.config files to live next to package.json.",
44
+ recommended: true
45
+ },
46
+ messages: { orphan: "Keep vite.config.ts next to package.json." }
47
+ },
48
+ create(context) {
49
+ return { Program(node) {
50
+ if (isViteConfigFile(context.filename) && !hasPackageJson(dirname(context.filename))) context.report({
51
+ node,
52
+ messageId: "orphan"
53
+ });
54
+ } };
55
+ }
56
+ }),
57
+ "no-useless-vp-preset-imports": defineRule({
58
+ meta: {
59
+ type: "problem",
60
+ docs: {
61
+ description: `Disallow importing ${packageName} outside vite.config files.`,
62
+ recommended: true
63
+ },
64
+ messages: { outsideConfig: `Import ${packageName} only from vite.config.ts.` }
65
+ },
66
+ create(context) {
67
+ const reportModuleReference = (node, specifier) => {
68
+ if (!isVpConfigSpecifier(specifier) || isViteConfigFile(context.filename)) return;
69
+ context.report({
70
+ node,
71
+ messageId: "outsideConfig"
72
+ });
73
+ };
74
+ return {
75
+ ImportDeclaration(node) {
76
+ reportModuleReference(node.source, node.source.value);
77
+ },
78
+ ExportAllDeclaration(node) {
79
+ reportModuleReference(node.source, node.source.value);
80
+ },
81
+ ExportNamedDeclaration(node) {
82
+ if (node.source) reportModuleReference(node.source, node.source.value);
83
+ },
84
+ ImportExpression(node) {
85
+ if (isStringLiteral(node.source)) reportModuleReference(node.source, node.source.value);
86
+ }
87
+ };
88
+ }
89
+ }),
90
+ "use-preset-vp-config": defineRule({
91
+ meta: {
92
+ type: "problem",
93
+ docs: {
94
+ description: "Require vite.config.ts to be loaded through @liangmi/vp-config.",
95
+ recommended: true
96
+ },
97
+ messages: { missingRuntimeInfo: `Load vite.config.ts through ${packageName} so lint rules can inspect runtime config.` }
98
+ },
99
+ create(context) {
100
+ return { Program(node) {
101
+ if (!canUseRuntimeInfo(context.filename)) return;
102
+ if (!ensureRuntimeInfo(context.filename)) context.report({
103
+ node,
104
+ messageId: "missingRuntimeInfo"
105
+ });
106
+ } };
107
+ }
108
+ }),
109
+ "load-proper-vp-config-category": defineRule({
110
+ meta: {
111
+ type: "problem",
112
+ docs: {
113
+ description: `Load ${packageName} with a category that matches the package role.`,
114
+ recommended: true
115
+ },
116
+ messages: { wrongCategory: `Use {{expected}} from ${packageName} in this vite.config.ts.` }
117
+ },
118
+ create(context) {
119
+ return { Program(node) {
120
+ if (!canUseRuntimeInfo(context.filename)) return;
121
+ const info = ensureRuntimeInfo(context.filename);
122
+ if (!info?.category) return;
123
+ const allowed = isProject(info) ? projectConfigNames : rootConfigNames;
124
+ if (allowed.includes(info.category)) return;
125
+ context.report({
126
+ node,
127
+ messageId: "wrongCategory",
128
+ data: { expected: allowed.map((name) => `{ ${name} }`).join(" or ") }
129
+ });
130
+ } };
131
+ }
132
+ }),
133
+ "no-mixed-project": defineRule({
134
+ meta: {
135
+ type: "problem",
136
+ docs: {
137
+ description: "Disallow a vite config that is both website and library project.",
138
+ recommended: true
139
+ },
140
+ messages: { mixed: "Do not mix website and library project signals in one vite.config.ts." }
141
+ },
142
+ create(context) {
143
+ return { Program(node) {
144
+ if (!canUseRuntimeInfo(context.filename)) return;
145
+ const info = ensureRuntimeInfo(context.filename);
146
+ if (info && isWebsiteProject(info) && isLibProject(info)) context.report({
147
+ node,
148
+ messageId: "mixed"
149
+ });
150
+ } };
151
+ }
152
+ })
153
+ };
154
+ //#endregion
155
+ //#region src/oxlint-plugin/index.ts
156
+ var oxlint_plugin_default = definePlugin({
157
+ meta: { name: pluginName },
158
+ rules
159
+ });
160
+ //#endregion
161
+ export { cleanupRuntimeInfo, oxlint_plugin_default as default, getAllowedConfigNames, getInfoPath, isVpConfigImportAllowed, readRuntimeInfo, writeRuntimeInfo };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liangmi/vp-config",
3
- "version": "0.0.0",
3
+ "version": "0.1.1",
4
4
  "description": "Liang's JavaScript development toolchain config with Vite+.",
5
5
  "homepage": "https://github.com/liangmiQwQ/vp-config#readme",
6
6
  "bugs": {
@@ -18,28 +18,28 @@
18
18
  "type": "module",
19
19
  "exports": {
20
20
  ".": "./dist/index.mjs",
21
+ "./oxlint-plugin": "./dist/oxlint-plugin/index.mjs",
21
22
  "./package.json": "./package.json"
22
23
  },
23
24
  "publishConfig": {
24
25
  "access": "public"
25
26
  },
26
27
  "devDependencies": {
27
- "@types/node": "^25.6.2",
28
- "@typescript/native-preview": "7.0.0-dev.20260509.2",
28
+ "@types/node": "^26.0.0",
29
+ "@typescript/native-preview": "7.0.0-dev.20260620.1",
29
30
  "bumpp": "^11.1.0",
30
- "tinyglobby": "^0.2.17",
31
31
  "typescript": "^6.0.3",
32
- "vite-plus": "^0.1.24",
33
- "yaml": "^2.9.0"
32
+ "vite-plus": "^0.2.1"
34
33
  },
35
34
  "peerDependencies": {
36
- "vite-plus": "^0.1.22"
35
+ "@oxlint/plugins": "1.61.0",
36
+ "vite-plus": ">=0.2.0"
37
37
  },
38
38
  "scripts": {
39
- "build": "vp pack",
40
- "check": "vp check",
39
+ "build": "vp run cpack",
40
+ "check": "vp run cpack && vp run ccheck",
41
41
  "dev": "vp pack --watch",
42
42
  "release": "bumpp",
43
- "test": "vp test"
43
+ "test": "vp run ctest"
44
44
  }
45
45
  }