create-vue-workspace 0.1.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
@@ -11,6 +11,7 @@
11
11
  ```
12
12
  cvw new [name] # 创建工作区
13
13
  cvw generate <type> [name] # 运行生成器,别名 cvw g
14
+ cvw update # 更新 cvw 自身到最新版本
14
15
  ```
15
16
 
16
17
  ### 生成器
@@ -71,9 +72,30 @@ cvw new # 追问:工程信息
71
72
  cvw g list # 打印生成器清单
72
73
  ```
73
74
 
75
+ ### 自更新
76
+
77
+ ```bash
78
+ cvw update # 检查并升级到最新版本(会先询问确认)
79
+ cvw update --check # 只检查,不升级
80
+ cvw update -y # 不询问,直接升级
81
+ cvw update --manager pnpm # 指定包管理器
82
+ ```
83
+
84
+ | 参数 | 说明 |
85
+ | --- | --- |
86
+ | `--check` | 只检查是否有新版本 |
87
+ | `--manager <npm\|pnpm\|yarn\|bun>` | 指定用于升级的包管理器 |
88
+ | `-y, --yes` | 不询问,直接升级 |
89
+
90
+ 说明:
91
+
92
+ - 升级的是 **CLI 自身**,不会改动已生成的工程
93
+ - 通过 `npx` / `pnpm dlx` 临时运行时会提示「无需手动更新」(这类方式每次都拉取最新版)
94
+ - 安装方式由可执行文件路径推断(识别 pnpm / yarn / bun 的特征目录),推断不出时回退 `npm`;**实际执行的命令会先打印出来**,避免装错位置
95
+
74
96
  ### 名称归一化
75
97
 
76
- 输入 `Button` / `button` / `input_group` 都会被归一化:目录 `button`、组件名 `FButton`、选择器 `f-button`、文件 `button.component.tsx`。
98
+ 输入 `Button` / `button` / `input_group` 都会被归一化:目录 `button`、组件名 `FButton`、选择器 `f-button`、组件文件 `components/button.component.tsx`。
77
99
 
78
100
  `cvw g h use-theme` 与 `cvw g h theme` 等价(自动去掉 `use` 前缀)。
79
101
 
@@ -94,10 +116,13 @@ my-app/
94
116
  │ ├── ui/ # @my-app/ui,组件聚合包
95
117
  │ ├── utils/ # @my-app/utils,纯 TS 工具
96
118
  │ └── button/ # cvw g c 生成,独立组件包
97
- │ ├── index.ts
98
- │ ├── button.component.tsx
99
- │ ├── button.props.ts
100
- ├── composition/
119
+ │ ├── index.ts # 入口:导出组件与 props 类型
120
+ │ ├── components/ # 组件实现
121
+ ├── button.component.tsx
122
+ │ └── button.props.ts
123
+ │ ├── composition/ # 可组合逻辑
124
+ │ ├── button.scss
125
+ │ ├── types.ts
101
126
  │ ├── __tests__/
102
127
  │ ├── package.json
103
128
  │ └── vite.config.ts
@@ -106,7 +131,7 @@ my-app/
106
131
  └── .changeset/
107
132
  ```
108
133
 
109
- 组件骨架遵循《Farris UI Vue 组件开发规范》:`index.ts` + `<name>.component.tsx` + `<name>.props.ts` + `composition/` + `__tests__/`,组件名 `F` 前缀、选择器 `f-` 前缀。
134
+ 组件骨架基于《Farris UI Vue 组件开发规范》,并按本仓库约定调整:组件实现(`<name>.component.tsx`、`<name>.props.ts`)统一放在 `components/` 下,与 `composition/`、`__tests__/` 平级;样式 `<name>.scss` 保留在组件包根目录。组件名 `F` 前缀、选择器 `f-` 前缀。
110
135
 
111
136
  ## 开发约定
112
137
 
package/dist/index.mjs CHANGED
@@ -8,7 +8,9 @@ import { fileURLToPath } from 'node:url';
8
8
  import { execa } from 'execa';
9
9
 
10
10
  const CLI_NAME = "cvw";
11
- const CLI_FULL_NAME = "create-vue-workspace";
11
+ const PACKAGE_NAME = "create-vue-workspace";
12
+ const CLI_FULL_NAME = PACKAGE_NAME;
13
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
12
14
  const APP_DIR_NAME = "web";
13
15
  const DEFAULT_PORT = 5173;
14
16
  const DEFAULT_PACKAGES = ["ui", "utils"];
@@ -233,13 +235,18 @@ function createComponentNaming(input) {
233
235
  };
234
236
  }
235
237
 
236
- function unwrap$1(value) {
238
+ function unwrap(value) {
237
239
  if (p.isCancel(value)) {
238
240
  p.cancel("\u5DF2\u53D6\u6D88\u3002");
239
241
  process.exit(0);
240
242
  }
241
243
  return value;
242
244
  }
245
+ async function confirmOrCancel(message) {
246
+ const answer = await p.confirm({ message });
247
+ return unwrap(answer);
248
+ }
249
+
243
250
  function normalizeScope(raw, projectName) {
244
251
  const value = (raw ?? "").trim() || projectName;
245
252
  return value.startsWith("@") ? value : `@${value}`;
@@ -262,16 +269,16 @@ async function resolveNewAnswers(nameOption, options) {
262
269
  let port = options.port ? Number(options.port) : DEFAULT_PORT;
263
270
  let ui = options.ui === "none" ? "none" : "farris";
264
271
  if (isInteractive(options)) {
265
- name = unwrap$1(
272
+ name = unwrap(
266
273
  await p.text({ message: "\u5DE5\u7A0B\u76EE\u5F55\u540D", placeholder: defaultName, defaultValue: defaultName })
267
274
  );
268
275
  scope = normalizeScope(
269
- unwrap$1(
276
+ unwrap(
270
277
  await p.text({ message: "\u5305 scope\uFF08\u751F\u6210\u7269\u5305\u540D\u524D\u7F00\uFF09", placeholder: scope, defaultValue: scope })
271
278
  ),
272
279
  projectName
273
280
  );
274
- packages = unwrap$1(
281
+ packages = unwrap(
275
282
  await p.multiselect({
276
283
  message: "\u521D\u59CB\u521B\u5EFA\u54EA\u4E9B packages",
277
284
  options: [
@@ -283,11 +290,11 @@ async function resolveNewAnswers(nameOption, options) {
283
290
  })
284
291
  );
285
292
  port = Number(
286
- unwrap$1(
293
+ unwrap(
287
294
  await p.text({ message: "\u5E94\u7528\u5F00\u53D1\u7AEF\u53E3", placeholder: String(port), defaultValue: String(port) })
288
295
  )
289
296
  );
290
- ui = unwrap$1(
297
+ ui = unwrap(
291
298
  await p.select({
292
299
  message: "UI \u57FA\u7840\u5E93",
293
300
  options: [
@@ -322,6 +329,16 @@ async function runCommand(command, args, cwd) {
322
329
  ${detail}`);
323
330
  }
324
331
  }
332
+ async function captureCommand(command, args, cwd) {
333
+ try {
334
+ const { stdout } = await execa(command, args, { cwd, stdio: "pipe" });
335
+ return stdout;
336
+ } catch (error) {
337
+ const detail = error instanceof Error ? error.message : String(error);
338
+ throw new Error(`\u6267\u884C\u547D\u4EE4\u5931\u8D25\uFF1A${command} ${args.join(" ")}
339
+ ${detail}`);
340
+ }
341
+ }
325
342
  async function tryRunCommand(command, args, cwd) {
326
343
  try {
327
344
  await execa(command, args, { cwd, stdio: "pipe" });
@@ -433,6 +450,129 @@ async function installDependencies(targetDir, answers) {
433
450
  spinner.stop("\u4F9D\u8D56\u5B89\u88C5\u5B8C\u6210");
434
451
  }
435
452
 
453
+ function parseVersion(version) {
454
+ return version.replace(/^v/i, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0).concat([0, 0, 0]).slice(0, 3);
455
+ }
456
+ function compareVersions(left, right) {
457
+ const leftParts = parseVersion(left);
458
+ const rightParts = parseVersion(right);
459
+ for (let index = 0; index < 3; index += 1) {
460
+ if (leftParts[index] !== rightParts[index]) {
461
+ return leftParts[index] > rightParts[index] ? 1 : -1;
462
+ }
463
+ }
464
+ return 0;
465
+ }
466
+ function isNewerVersion(candidate, current) {
467
+ return compareVersions(candidate, current) > 0;
468
+ }
469
+
470
+ const PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
471
+ const GLOBAL_INSTALL_ARGS = {
472
+ npm: ["install", "-g", `${PACKAGE_NAME}@latest`],
473
+ pnpm: ["add", "-g", `${PACKAGE_NAME}@latest`],
474
+ yarn: ["global", "add", `${PACKAGE_NAME}@latest`],
475
+ bun: ["add", "-g", `${PACKAGE_NAME}@latest`]
476
+ };
477
+ function normalizeScriptPath() {
478
+ return (process.argv[1] ?? "").replace(/\\/g, "/");
479
+ }
480
+ function detectManager() {
481
+ const scriptPath = normalizeScriptPath();
482
+ if (scriptPath.includes("/.pnpm/") || scriptPath.includes("/pnpm/")) {
483
+ return "pnpm";
484
+ }
485
+ if (scriptPath.includes("/.yarn/") || scriptPath.includes("/yarn/")) {
486
+ return "yarn";
487
+ }
488
+ if (scriptPath.includes("/.bun/") || scriptPath.includes("/bun/")) {
489
+ return "bun";
490
+ }
491
+ return "npm";
492
+ }
493
+ function resolveManager(raw) {
494
+ if (!raw) {
495
+ return detectManager();
496
+ }
497
+ const value = raw.trim().toLowerCase();
498
+ if (!PACKAGE_MANAGERS.includes(value)) {
499
+ throw new Error(`\u4E0D\u652F\u6301\u7684\u5305\u7BA1\u7406\u5668 "${raw}"\uFF0C\u53EF\u9009\uFF1A${PACKAGE_MANAGERS.join(" / ")}`);
500
+ }
501
+ return value;
502
+ }
503
+ function buildUpgradeCommand(manager) {
504
+ return [manager, ...GLOBAL_INSTALL_ARGS[manager]].join(" ");
505
+ }
506
+ function isEphemeralRun() {
507
+ const scriptPath = normalizeScriptPath();
508
+ return scriptPath.includes("/_npx/") || scriptPath.includes("/dlx/");
509
+ }
510
+ async function resolveRegistry() {
511
+ try {
512
+ const registry = (await captureCommand("npm", ["config", "get", "registry"], process.cwd())).trim();
513
+ return registry && registry !== "undefined" ? registry : DEFAULT_REGISTRY;
514
+ } catch {
515
+ return DEFAULT_REGISTRY;
516
+ }
517
+ }
518
+ async function fetchLatestVersion(registry) {
519
+ const url = `${registry.replace(/\/+$/, "")}/${PACKAGE_NAME}`;
520
+ const response = await fetch(url, {
521
+ headers: { accept: "application/vnd.npm.install-v1+json" }
522
+ });
523
+ if (response.status === 404) {
524
+ throw new Error(`registry\uFF08${registry}\uFF09\u4E0A\u672A\u627E\u5230 ${PACKAGE_NAME}\uFF0C\u53EF\u80FD\u5C1A\u672A\u53D1\u5E03\u3002`);
525
+ }
526
+ if (!response.ok) {
527
+ throw new Error(`\u67E5\u8BE2\u6700\u65B0\u7248\u672C\u5931\u8D25\uFF1AHTTP ${response.status}\uFF08${url}\uFF09`);
528
+ }
529
+ const metadata = await response.json();
530
+ const latest = metadata["dist-tags"]?.latest;
531
+ if (!latest) {
532
+ throw new Error(`\u672A\u80FD\u4ECE registry \u89E3\u6790 ${PACKAGE_NAME} \u7684 latest \u7248\u672C\u3002`);
533
+ }
534
+ return latest;
535
+ }
536
+ async function updateCommand(options) {
537
+ p.intro(pc.bgCyan(pc.black(` ${CLI_NAME} update `)));
538
+ const manager = resolveManager(options.manager);
539
+ const spinner = p.spinner();
540
+ spinner.start(`\u68C0\u67E5 ${PACKAGE_NAME} \u7684\u6700\u65B0\u7248\u672C`);
541
+ const registry = await resolveRegistry();
542
+ let latest;
543
+ try {
544
+ latest = await fetchLatestVersion(registry);
545
+ } catch (error) {
546
+ spinner.stop("\u68C0\u67E5\u5931\u8D25");
547
+ throw error;
548
+ }
549
+ spinner.stop(`\u5F53\u524D ${VERSION} \xB7 \u6700\u65B0 ${latest}`);
550
+ if (!isNewerVersion(latest, VERSION)) {
551
+ p.outro(pc.green("\u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF0C\u65E0\u9700\u66F4\u65B0"));
552
+ return;
553
+ }
554
+ if (isEphemeralRun()) {
555
+ p.log.info("\u5F53\u524D\u901A\u8FC7 npx / pnpm dlx \u4E34\u65F6\u8FD0\u884C\uFF0C\u8FD9\u7C7B\u65B9\u5F0F\u6BCF\u6B21\u90FD\u4F1A\u62C9\u53D6\u6700\u65B0\u7248\uFF0C\u65E0\u9700\u624B\u52A8\u66F4\u65B0\u3002");
556
+ p.outro(pc.dim(`\u5982\u9700\u56FA\u5B9A\u5B89\u88C5\uFF1A${buildUpgradeCommand("npm")}`));
557
+ return;
558
+ }
559
+ const upgradeCommand = buildUpgradeCommand(manager);
560
+ p.log.info(`\u5347\u7EA7\u547D\u4EE4\uFF1A${pc.cyan(upgradeCommand)}`);
561
+ if (options.check) {
562
+ p.outro(pc.yellow(`\u53D1\u73B0\u65B0\u7248\u672C ${latest}\uFF08\u5F53\u524D ${VERSION}\uFF09\uFF0C\u5DF2\u6309 --check \u8DF3\u8FC7\u5347\u7EA7`));
563
+ return;
564
+ }
565
+ if (!options.yes) {
566
+ const confirmed = await confirmOrCancel(`\u662F\u5426\u73B0\u5728\u5347\u7EA7\u5230 ${latest}\uFF1F`);
567
+ if (!confirmed) {
568
+ p.outro("\u5DF2\u53D6\u6D88");
569
+ return;
570
+ }
571
+ }
572
+ await runCommand(manager, GLOBAL_INSTALL_ARGS[manager], process.cwd());
573
+ p.outro(pc.green(`\u5347\u7EA7\u5B8C\u6210\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C ${CLI_NAME} --version \u786E\u8BA4\u7248\u672C`));
574
+ }
575
+
436
576
  function stringify(manifest) {
437
577
  return `${JSON.stringify(manifest, null, 2)}
438
578
  `;
@@ -611,7 +751,7 @@ function componentGenerator(inputName, options, context) {
611
751
  const packageName = `${scope}/${naming.packageName}`;
612
752
  const skip = [];
613
753
  if (!options.withSub) {
614
- skip.push("components");
754
+ skip.push("components/{{fileName}}-sub.component.tsx");
615
755
  }
616
756
  if (options.skipTests) {
617
757
  skip.push("__tests__");
@@ -824,13 +964,6 @@ function formatGeneratorList() {
824
964
  ).join("\n");
825
965
  }
826
966
 
827
- function unwrap(value) {
828
- if (p.isCancel(value)) {
829
- p.cancel("\u5DF2\u53D6\u6D88\u3002");
830
- process.exit(0);
831
- }
832
- return value;
833
- }
834
967
  async function resolveGenerateInput(type, name, options) {
835
968
  const interactive = !options.yes && Boolean(process.stdout.isTTY);
836
969
  let generator = type ? resolveGenerator(type) : void 0;
@@ -900,6 +1033,9 @@ function runCli(argv = process.argv) {
900
1033
  cli.command("new [name]", "\u521B\u5EFA Vue 3 + Vite + TypeScript \u7684 pnpm monorepo \u5DE5\u4F5C\u533A").option("--scope <scope>", "\u751F\u6210\u7269\u7684\u5305 scope\uFF0C\u5982 @my-app").option("--packages <list>", "\u521D\u59CB\u521B\u5EFA\u7684 packages\uFF0C\u9017\u53F7\u5206\u9694", { default: "ui,utils" }).option("--ui <provider>", "UI \u57FA\u7840\u5E93\uFF1Afarris \u6216 none", { default: "farris" }).option("--port <port>", "\u5E94\u7528\u5F00\u53D1\u7AEF\u53E3").option("--skip-install", "\u8DF3\u8FC7\u4F9D\u8D56\u5B89\u88C5").option("--skip-git", "\u8DF3\u8FC7 git \u521D\u59CB\u5316").option("--dry-run", "\u53EA\u6253\u5370\u5C06\u751F\u6210\u7684\u6587\u4EF6\uFF0C\u4E0D\u843D\u76D8").option("-y, --yes", "\u5168\u90E8\u4F7F\u7528\u9ED8\u8BA4\u503C\uFF0C\u975E\u4EA4\u4E92\u6267\u884C").action((name, options) => {
901
1034
  handleCommand(() => newCommand(name, options));
902
1035
  });
1036
+ cli.command("update", "\u66F4\u65B0 cvw \u81EA\u8EAB\u5230\u6700\u65B0\u7248\u672C\uFF08\u4E0D\u5F71\u54CD\u5DF2\u751F\u6210\u7684\u5DE5\u7A0B\uFF09").option("--check", "\u53EA\u68C0\u67E5\u662F\u5426\u6709\u65B0\u7248\u672C\uFF0C\u4E0D\u6267\u884C\u5347\u7EA7").option("--manager <manager>", "\u6307\u5B9A\u5305\u7BA1\u7406\u5668\uFF1Anpm / pnpm / yarn / bun").option("-y, --yes", "\u4E0D\u8BE2\u95EE\uFF0C\u76F4\u63A5\u5347\u7EA7").action((options) => {
1037
+ handleCommand(() => updateCommand(options));
1038
+ });
903
1039
  registerGenerate(cli, "generate", true);
904
1040
  registerGenerate(cli, "g", false);
905
1041
  cli.help();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-vue-workspace",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Scaffold a Vue 3 pnpm monorepo (apps/* + packages/*) with independently publishable component packages.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { describe, expect, it } from 'vitest';
3
- import {{pascalName}} from '../{{fileName}}.component';
3
+ import {{pascalName}} from '../components/{{fileName}}.component';
4
4
 
5
5
  describe('{{selector}}', () => {
6
6
  it('默认渲染根节点', () => {
@@ -1,8 +1,8 @@
1
1
  import { defineComponent, ref, type SetupContext } from 'vue';
2
+ import { use{{pascalName}}Composition } from '../composition/use-{{fileName}}';
2
3
  import { {{propsName}}, {{propsTypeName}} } from './{{fileName}}.props';
3
- import { use{{pascalName}}Composition } from './composition/use-{{fileName}}';
4
4
 
5
- import './{{fileName}}.scss';
5
+ import '../{{fileName}}.scss';
6
6
 
7
7
  export default defineComponent({
8
8
  name: '{{componentName}}',
@@ -1,5 +1,5 @@
1
1
  import { computed, type ComputedRef, type Ref, type SetupContext } from 'vue';
2
- import type { {{propsTypeName}} } from '../{{fileName}}.props';
2
+ import type { {{propsTypeName}} } from '../components/{{fileName}}.props';
3
3
 
4
4
  export interface Use{{pascalName}} {
5
5
  /** 组件根节点样式 */
@@ -1,7 +1,7 @@
1
1
  import type { App } from 'vue';
2
- import {{pascalName}} from './{{fileName}}.component';
2
+ import {{pascalName}} from './components/{{fileName}}.component';
3
3
 
4
- export * from './{{fileName}}.props';
4
+ export * from './components/{{fileName}}.props';
5
5
 
6
6
  export { {{pascalName}} };
7
7