create-template-project 1.5.23 → 1.6.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.
Files changed (26) hide show
  1. package/README.md +39 -5
  2. package/dist/config/dependencies.json +24 -4
  3. package/dist/index.js +733 -142
  4. package/dist/templates/base/files/README.md +1 -1
  5. package/dist/templates/base/files/_oxc.config.ts +17 -35
  6. package/dist/templates/web-widget/files/.github/workflows/pages.yml +36 -0
  7. package/dist/templates/web-widget/files/index.html +21 -0
  8. package/dist/templates/web-widget/files/package.json +53 -0
  9. package/dist/templates/web-widget/files/playwright.config.ts +29 -0
  10. package/dist/templates/web-widget/files/scripts/screenshot.mjs +35 -0
  11. package/dist/templates/web-widget/files/src/demo/app.ts +22 -0
  12. package/dist/templates/web-widget/files/src/demo/demo-shell.css +50 -0
  13. package/dist/templates/web-widget/files/src/lib/index.ts +2 -0
  14. package/dist/templates/web-widget/files/src/lib/react.tsx +54 -0
  15. package/dist/templates/web-widget/files/src/lib/test-setup.ts +7 -0
  16. package/dist/templates/web-widget/files/src/lib/widget.test.ts +33 -0
  17. package/dist/templates/web-widget/files/src/lib/widget.ts +83 -0
  18. package/dist/templates/web-widget/files/src/styles/index.css +42 -0
  19. package/dist/templates/web-widget/files/stylelint.base.config.js +16 -0
  20. package/dist/templates/web-widget/files/stylelint.config.js +15 -0
  21. package/dist/templates/web-widget/files/tests/e2e/widget.e2e-test.ts +16 -0
  22. package/dist/templates/web-widget/files/tsdown.config.ts +20 -0
  23. package/dist/templates/web-widget/files/typedoc.json +12 -0
  24. package/dist/templates/web-widget/files/vite.config.ts +37 -0
  25. package/package.json +7 -5
  26. /package/dist/templates/base/files/.github/workflows/{node.js.yml → ci.yml} +0 -0
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { existsSync } from "node:fs";
12
12
  var TemplateTypeSchema = z.enum([
13
13
  "cli",
14
14
  "web-vanilla",
15
+ "web-widget",
15
16
  "web-app",
16
17
  "web-fullstack"
17
18
  ]);
@@ -46,6 +47,9 @@ var genericProcessor = (content, { opts }) => {
46
47
  case "web-vanilla":
47
48
  description = "A standalone web page/application for modern browsers.";
48
49
  break;
50
+ case "web-widget":
51
+ description = "A publishable native TypeScript web widget with a Vite demo and npm-ready package build.";
52
+ break;
49
53
  case "web-fullstack":
50
54
  description = "A full-stack monorepo with an Express server and a React/MUI client.";
51
55
  break;
@@ -68,7 +72,7 @@ var WORKFLOW_PNPM_SETUP = ` - name: Setup pnpm
68
72
  var WORKFLOW_PLAYWRIGHT_SETUP = ` - name: Install Playwright Browsers & Deps
69
73
  run: npx playwright install --with-deps chromium`;
70
74
  var githubWorkflowProcessor = (content, { filePath, opts }) => {
71
- if (!filePath.includes(".github/workflows/node.js.yml")) return content;
75
+ if (!filePath.includes(".github/workflows/") || !filePath.endsWith(".yml")) return content;
72
76
  const { template, packageManager: pm } = opts;
73
77
  let installCommand = "npm ci";
74
78
  let pmSetup = "";
@@ -77,7 +81,7 @@ var githubWorkflowProcessor = (content, { filePath, opts }) => {
77
81
  pmSetup = WORKFLOW_PNPM_SETUP;
78
82
  } else if (pm === "yarn") installCommand = "yarn install --frozen-lockfile";
79
83
  let playwrightSetup = "";
80
- if (template === "web-fullstack" || template === "web-app" || template === "web-vanilla") playwrightSetup = WORKFLOW_PLAYWRIGHT_SETUP;
84
+ if ((filePath.includes("ci.yml") || filePath.includes("node.js.yml")) && (template === "web-fullstack" || template === "web-app" || template === "web-vanilla" || template === "web-widget")) playwrightSetup = WORKFLOW_PLAYWRIGHT_SETUP;
81
85
  let processed = content.replaceAll("{{installCommand}}", installCommand).replaceAll("# [PM_SETUP]", pmSetup).replaceAll("# [PLAYWRIGHT_SETUP]", playwrightSetup);
82
86
  processed = processed.replace(/^\s*# \[PM_SETUP\]\s*\n/mu, "");
83
87
  processed = processed.replace(/^\s*# \[PLAYWRIGHT_SETUP\]\s*\n/mu, "");
@@ -99,7 +103,11 @@ var tsconfigProcessor = (content, { filePath, opts }) => {
99
103
  if (filePath !== "tsconfig.json") return content;
100
104
  const { template } = opts;
101
105
  let processed = content;
102
- if (template === "web-fullstack" || template === "web-vanilla" || template === "web-app") processed = processed.replace(/\/\* Language and Environment \*\/[\s\S]*?\/\* Strict Type-Checking Options \*\//u, `${WEB_ENV}\n\n\t\t/* Strict Type-Checking Options */`);
106
+ if (template === "web-fullstack" || template === "web-vanilla" || template === "web-widget" || template === "web-app") processed = processed.replace(/\/\* Language and Environment \*\/[\s\S]*?\/\* Strict Type-Checking Options \*\//u, `${WEB_ENV}\n\n\t\t/* Strict Type-Checking Options */`);
107
+ if (template === "web-widget") processed = processed.replace("\"moduleResolution\": \"bundler\" /* Specify how TypeScript looks up a file from a given module specifier. */,", `"moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */,
108
+ "paths": {
109
+ "${opts.projectName}": ["./src/lib/index.ts"]
110
+ },`);
103
111
  if (template === "web-fullstack") processed = processed.replace(/"include":\s*\[\s*"src\/\*\*\/\*"\s*\]/u, "\"include\": [\"client/src/**/*\", \"server/src/**/*\"]");
104
112
  return processed;
105
113
  };
@@ -124,7 +132,7 @@ var webVanillaHtmlProcessor = (content, { filePath, opts }) => {
124
132
  var oxcConfigProcessor = (content, { filePath, opts }) => {
125
133
  if (filePath !== "oxc.config.ts") return content;
126
134
  const shouldAddNodeEnv = opts.template === "cli" || opts.template === "web-fullstack";
127
- const shouldAddBrowserEnv = opts.template === "web-vanilla" || opts.template === "web-app" || opts.template === "web-fullstack";
135
+ const shouldAddBrowserEnv = opts.template === "web-vanilla" || opts.template === "web-widget" || opts.template === "web-app" || opts.template === "web-fullstack";
128
136
  const linesToAdd = [];
129
137
  if (shouldAddNodeEnv && !content.includes("node: true")) linesToAdd.push(" node: true,");
130
138
  if (shouldAddBrowserEnv && !content.includes("browser: true")) linesToAdd.push(" browser: true,");
@@ -155,6 +163,34 @@ var processContent$1 = (filePath, content, opts, addedDeps) => {
155
163
  //#endregion
156
164
  //#region src/shared/file.ts
157
165
  var debug$3 = debugLib("create-template-project:utils:file");
166
+ var parseExactVersion$1 = (version) => {
167
+ const match = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:[-+].*)?$/u.exec(version);
168
+ if (match?.groups === void 0) return;
169
+ return [
170
+ Number(match.groups.major),
171
+ Number(match.groups.minor),
172
+ Number(match.groups.patch)
173
+ ];
174
+ };
175
+ var isGreaterOrEqualVersion$1 = (current, next) => {
176
+ const currentParts = parseExactVersion$1(current);
177
+ const nextParts = parseExactVersion$1(next);
178
+ if (currentParts === void 0 || nextParts === void 0) return false;
179
+ for (const [index, currentPart] of currentParts.entries()) {
180
+ const nextPart = nextParts[index];
181
+ if (currentPart > nextPart) return true;
182
+ if (currentPart < nextPart) return false;
183
+ }
184
+ return true;
185
+ };
186
+ var mergeStringRecord = (target, source, preserveExistingVersions) => {
187
+ const merged = { ...target };
188
+ for (const [name, version] of Object.entries(source)) {
189
+ if (preserveExistingVersions && Object.hasOwn(merged, name) && isGreaterOrEqualVersion$1(merged[name], version)) continue;
190
+ merged[name] = version;
191
+ }
192
+ return merged;
193
+ };
158
194
  var toErrorDetail$1 = (error) => {
159
195
  if (error instanceof Error) {
160
196
  const value = error;
@@ -191,21 +227,27 @@ var getAllFiles = async (dirPath, arrayOfFiles = []) => {
191
227
  ];
192
228
  };
193
229
  var processContent = (filePath, content, opts, addedDeps) => processContent$1(filePath, content, opts, addedDeps);
194
- var mergePackageJson = (target, source) => {
230
+ var mergePackageJson = (target, source, options = {}) => {
231
+ const preservePackageFields = options.preserveExistingPackageFields === true;
232
+ const preserveDependencyVersions = options.preserveExistingDependencyVersions === true;
233
+ if (source.private !== void 0) target.private = source.private;
234
+ if (source.files !== void 0 && (!preservePackageFields || target.files === void 0)) target.files = source.files;
235
+ if (source.main !== void 0 && (!preservePackageFields || target.main === void 0)) target.main = source.main;
236
+ if (source.module !== void 0 && (!preservePackageFields || target.module === void 0)) target.module = source.module;
237
+ if (source.types !== void 0 && (!preservePackageFields || target.types === void 0)) target.types = source.types;
238
+ if (source.exports !== void 0 && (!preservePackageFields || target.exports === void 0)) target.exports = source.exports;
195
239
  if (source.scripts !== void 0) target.scripts = {
196
240
  ...target.scripts,
197
241
  ...source.scripts
198
242
  };
199
- if (source.dependencies !== void 0) target.dependencies = {
200
- ...target.dependencies,
201
- ...source.dependencies
243
+ if (source.dependencies !== void 0) target.dependencies = mergeStringRecord(target.dependencies, source.dependencies, preserveDependencyVersions);
244
+ if (source.devDependencies !== void 0) target.devDependencies = mergeStringRecord(target.devDependencies, source.devDependencies, preserveDependencyVersions);
245
+ if (source.peerDependencies !== void 0) target.peerDependencies = {
246
+ ...target.peerDependencies,
247
+ ...source.peerDependencies
202
248
  };
203
- if (source.devDependencies !== void 0) target.devDependencies = {
204
- ...target.devDependencies,
205
- ...source.devDependencies
206
- };
207
- if (source.workspaces !== void 0) target.workspaces = source.workspaces;
208
- if (source.bin !== void 0) target.bin = source.bin;
249
+ if (source.workspaces !== void 0 && (!preservePackageFields || target.workspaces === void 0)) target.workspaces = source.workspaces;
250
+ if (source.bin !== void 0 && (!preservePackageFields || target.bin === void 0)) target.bin = source.bin;
209
251
  };
210
252
  var isSeedFile = (filePath) => {
211
253
  return [
@@ -377,6 +419,72 @@ var getWebVanillaTemplate = (_opts) => ({
377
419
  templateDir: getTemplateDir(import.meta.dirname, "web-vanilla")
378
420
  });
379
421
  //#endregion
422
+ //#region src/templates/web-widget/index.ts
423
+ var getWebWidgetTemplate = (_opts) => ({
424
+ name: "web-widget",
425
+ description: "A publishable native TypeScript web widget with a Vite demo, package build, API docs, and screenshots.",
426
+ components: [
427
+ {
428
+ name: "Native TypeScript Widget",
429
+ description: "Dependency-free DOM component published as an ESM package."
430
+ },
431
+ {
432
+ name: "tsdown",
433
+ description: "Builds ESM library artifacts and declaration files for npm publishing."
434
+ },
435
+ {
436
+ name: "Vite",
437
+ description: "Runs and builds the documentation/demo site."
438
+ },
439
+ {
440
+ name: "TypeDoc",
441
+ description: "Generates API documentation for the public package surface."
442
+ },
443
+ {
444
+ name: "Stylelint",
445
+ description: "Lints the widget and demo stylesheets."
446
+ },
447
+ {
448
+ name: "Playwright",
449
+ description: "Runs e2e checks and captures demo screenshots."
450
+ }
451
+ ],
452
+ dependencies: {},
453
+ devDependencies: {
454
+ vite: "vite",
455
+ vitest: "vitest",
456
+ "@vitest/browser": "@vitest/browser",
457
+ "@vitest/browser-playwright": "@vitest/browser-playwright",
458
+ playwright: "playwright",
459
+ "@playwright/test": "@playwright/test",
460
+ tsdown: "tsdown",
461
+ typedoc: "typedoc",
462
+ stylelint: "stylelint",
463
+ "stylelint-config-standard": "stylelint-config-standard",
464
+ react: "react",
465
+ "react-dom": "react-dom",
466
+ "@types/react": "@types/react",
467
+ "@types/react-dom": "@types/react-dom"
468
+ },
469
+ scripts: {
470
+ dev: "vite",
471
+ build: "vite build",
472
+ "build:lib": "tsdown",
473
+ "build:demo": "vite build",
474
+ "docs:api": "typedoc",
475
+ preview: "vite preview",
476
+ start: "vite preview --port 4173 --strictPort",
477
+ test: "vitest run",
478
+ "test:ui": "vitest",
479
+ "integration-test": "playwright test",
480
+ "lint:css": "stylelint 'src/{demo,styles}/**/*.css'",
481
+ "lint:css:fix": "stylelint --fix 'src/{demo,styles}/**/*.css'",
482
+ screenshot: "node scripts/screenshot.mjs"
483
+ },
484
+ files: [],
485
+ templateDir: getTemplateDir(import.meta.dirname, "web-widget")
486
+ });
487
+ //#endregion
380
488
  //#region src/templates/web-app/index.ts
381
489
  var getWebAppTemplate = (_opts) => ({
382
490
  name: "web-app",
@@ -518,6 +626,7 @@ var getWebFullstackTemplate = (opts) => ({
518
626
  var templateFactories = {
519
627
  cli: getCliTemplate,
520
628
  "web-vanilla": getWebVanillaTemplate,
629
+ "web-widget": getWebWidgetTemplate,
521
630
  "web-app": getWebAppTemplate,
522
631
  "web-fullstack": getWebFullstackTemplate
523
632
  };
@@ -526,6 +635,7 @@ var getProjectTemplates = (opts) => [getBaseTemplate(opts), getTemplateByType(op
526
635
  var getTemplateTypes = () => [
527
636
  "cli",
528
637
  "web-vanilla",
638
+ "web-widget",
529
639
  "web-app",
530
640
  "web-fullstack"
531
641
  ];
@@ -558,6 +668,208 @@ var getTemplateInfo = (type) => {
558
668
  };
559
669
  var getAllTemplatesInfo = () => getTemplateTypes().map((type) => getTemplateInfo(type));
560
670
  //#endregion
671
+ //#region src/generators/project-compatibility.ts
672
+ var PASS_THRESHOLD = 75;
673
+ var isRecord$3 = (value) => typeof value === "object" && value !== null;
674
+ var pathExists$2 = async (filePath) => {
675
+ try {
676
+ await fs.access(filePath);
677
+ return true;
678
+ } catch {
679
+ return false;
680
+ }
681
+ };
682
+ var listFiles = async (directory) => {
683
+ try {
684
+ const entries = await fs.readdir(directory, { withFileTypes: true });
685
+ return (await Promise.all(entries.map(async (entry) => {
686
+ const entryPath = path.join(directory, entry.name);
687
+ if (entry.isDirectory()) return await listFiles(entryPath);
688
+ return entry.isFile() ? [entryPath] : [];
689
+ }))).flat();
690
+ } catch {
691
+ return [];
692
+ }
693
+ };
694
+ var parseStringRecord$1 = (value) => {
695
+ if (!isRecord$3(value)) return;
696
+ const parsed = {};
697
+ for (const [key, entry] of Object.entries(value)) if (typeof entry === "string") parsed[key] = entry;
698
+ return parsed;
699
+ };
700
+ var parsePackageJson = async (pkgPath) => {
701
+ try {
702
+ const parsed = JSON.parse(await fs.readFile(pkgPath, "utf8"));
703
+ if (!isRecord$3(parsed)) return;
704
+ return {
705
+ name: typeof parsed.name === "string" ? parsed.name : void 0,
706
+ type: typeof parsed.type === "string" ? parsed.type : void 0,
707
+ scripts: parseStringRecord$1(parsed.scripts),
708
+ dependencies: parseStringRecord$1(parsed.dependencies),
709
+ devDependencies: parseStringRecord$1(parsed.devDependencies),
710
+ workspaces: Array.isArray(parsed.workspaces) ? parsed.workspaces.filter((workspace) => typeof workspace === "string") : void 0
711
+ };
712
+ } catch {
713
+ return;
714
+ }
715
+ };
716
+ var hasAnyDependency = (pkg, dependency) => pkg?.dependencies?.[dependency] !== void 0 || pkg?.devDependencies?.[dependency] !== void 0;
717
+ var requiredFilesByTemplate = {
718
+ cli: [
719
+ "src/index.ts",
720
+ "src/index.test.ts",
721
+ "vite.config.ts"
722
+ ],
723
+ "web-vanilla": [
724
+ "index.html",
725
+ "src/index.ts",
726
+ "vite.config.ts",
727
+ "playwright.config.ts"
728
+ ],
729
+ "web-widget": [
730
+ "index.html",
731
+ "src/lib/index.ts",
732
+ "tsdown.config.ts",
733
+ "typedoc.json",
734
+ "vite.config.ts",
735
+ "playwright.config.ts"
736
+ ],
737
+ "web-app": [
738
+ "index.html",
739
+ "src/index.tsx",
740
+ "src/App.tsx",
741
+ "vite.config.ts",
742
+ "playwright.config.ts"
743
+ ],
744
+ "web-fullstack": [
745
+ "client/package.json",
746
+ "server/package.json",
747
+ "client/src/main.tsx",
748
+ "server/src/index.ts"
749
+ ]
750
+ };
751
+ var requiredDependenciesByTemplate = {
752
+ cli: ["vite"],
753
+ "web-vanilla": [
754
+ "vite",
755
+ "vitest",
756
+ "playwright"
757
+ ],
758
+ "web-widget": [
759
+ "vite",
760
+ "vitest",
761
+ "playwright",
762
+ "tsdown",
763
+ "typedoc",
764
+ "stylelint"
765
+ ],
766
+ "web-app": [
767
+ "react",
768
+ "react-dom",
769
+ "@mui/material",
770
+ "vite"
771
+ ],
772
+ "web-fullstack": [
773
+ "express",
774
+ "@trpc/server",
775
+ "react",
776
+ "react-dom"
777
+ ]
778
+ };
779
+ var validateProjectCompatibility = async (directory, template) => {
780
+ const pkg = await parsePackageJson(path.join(directory, "package.json"));
781
+ const checks = [];
782
+ const addCheck = (id, label, passed, required = false) => {
783
+ checks.push({
784
+ id,
785
+ label,
786
+ passed,
787
+ required
788
+ });
789
+ };
790
+ addCheck("package-json", "package.json exists and is valid JSON", pkg !== void 0, true);
791
+ addCheck("package-name", "package.json has a name", typeof pkg?.name === "string" && pkg.name.length > 0, true);
792
+ addCheck("esm", "package.json uses ESM type module", pkg?.type === "module");
793
+ const canonicalWorkflowPath = ".github/workflows/ci.yml";
794
+ const legacyWorkflowPath = ".github/workflows/node.js.yml";
795
+ const canonicalWorkflowExists = await pathExists$2(path.join(directory, canonicalWorkflowPath));
796
+ const legacyWorkflowExists = await pathExists$2(path.join(directory, legacyWorkflowPath));
797
+ const commonFileResults = await Promise.all([
798
+ "tsconfig.json",
799
+ "vitest.config.ts",
800
+ "commitlint.config.js",
801
+ ".husky/pre-commit"
802
+ ].map(async (filePath) => ({
803
+ filePath,
804
+ exists: await pathExists$2(path.join(directory, filePath))
805
+ })));
806
+ for (const { filePath, exists } of commonFileResults) addCheck(`file:${filePath}`, `${filePath} exists`, exists);
807
+ addCheck(`file:${canonicalWorkflowPath}`, canonicalWorkflowExists ? `${canonicalWorkflowPath} exists` : `${canonicalWorkflowPath} exists (canonical; legacy ${legacyWorkflowPath} is accepted for adoption)`, canonicalWorkflowExists || legacyWorkflowExists);
808
+ const configFileResults = await Promise.all([
809
+ "oxc.config.ts",
810
+ "oxlint.config.ts",
811
+ "oxfmt.config.ts"
812
+ ].map(async (filePath) => ({
813
+ filePath,
814
+ exists: await pathExists$2(path.join(directory, filePath))
815
+ })));
816
+ for (const { filePath, exists } of configFileResults) addCheck(`file:${filePath}`, `${filePath} exists`, exists);
817
+ for (const script of [
818
+ "typecheck",
819
+ "lint",
820
+ "format:check",
821
+ "ci",
822
+ "test"
823
+ ]) addCheck(`script:${script}`, `package.json has ${script} script`, pkg?.scripts?.[script] !== void 0);
824
+ for (const dependency of [
825
+ "typescript",
826
+ "vitest",
827
+ "oxlint",
828
+ "oxfmt",
829
+ "husky",
830
+ "@commitlint/cli"
831
+ ]) addCheck(`dependency:${dependency}`, `${dependency} dependency is declared`, hasAnyDependency(pkg, dependency));
832
+ const templateFileResults = await Promise.all(requiredFilesByTemplate[template].map(async (filePath) => ({
833
+ filePath,
834
+ exists: await pathExists$2(path.join(directory, filePath))
835
+ })));
836
+ for (const { filePath, exists } of templateFileResults) addCheck(`template-file:${filePath}`, `${template} file ${filePath} exists`, exists);
837
+ if (template === "web-widget") {
838
+ const sourceFiles = await listFiles(path.join(directory, "src/lib"));
839
+ const styleFiles = await listFiles(path.join(directory, "src/styles"));
840
+ const implementationFiles = sourceFiles.map((filePath) => path.relative(directory, filePath).split(path.sep).join("/")).filter((filePath) => /\.tsx?$/u.test(filePath) && !filePath.endsWith("/index.ts") && !filePath.endsWith("/react.tsx") && !filePath.includes(".test."));
841
+ const widgetStyles = styleFiles.map((filePath) => path.relative(directory, filePath).split(path.sep).join("/")).filter((filePath) => filePath.endsWith(".css"));
842
+ addCheck("web-widget:implementation", implementationFiles.length > 0 ? `web-widget implementation found (${implementationFiles.join(", ")})` : "web-widget implementation exists in src/lib", implementationFiles.length > 0);
843
+ addCheck("web-widget:stylesheet", widgetStyles.length > 0 ? `web-widget stylesheet found (${widgetStyles.join(", ")})` : "web-widget stylesheet exists in src/styles", widgetStyles.length > 0);
844
+ }
845
+ for (const dependency of requiredDependenciesByTemplate[template]) addCheck(`template-dependency:${dependency}`, `${template} dependency ${dependency} is declared`, hasAnyDependency(pkg, dependency));
846
+ if (template === "web-fullstack") addCheck("workspaces", "package.json has workspaces", Array.isArray(pkg?.workspaces) && pkg.workspaces.length > 0);
847
+ const requiredPassed = checks.filter((check) => check.required).every((check) => check.passed);
848
+ const passedCount = checks.filter((check) => check.passed).length;
849
+ const score = Math.round(passedCount / checks.length * 100);
850
+ return {
851
+ directory,
852
+ template,
853
+ score,
854
+ passed: requiredPassed && score >= PASS_THRESHOLD,
855
+ checks
856
+ };
857
+ };
858
+ var formatCompatibilityReport = (report) => {
859
+ const failed = report.checks.filter((check) => !check.passed);
860
+ const lines = [
861
+ `Directory: ${report.directory}`,
862
+ `Template: ${report.template}`,
863
+ `Score: ${report.score}%`,
864
+ `Status: ${report.passed ? "PASS" : "FAIL"}`
865
+ ];
866
+ if (failed.length > 0) {
867
+ lines.push("", "Missing or incompatible checks:");
868
+ for (const check of failed) lines.push(`- ${check.label}${check.required ? " (required)" : ""}`);
869
+ }
870
+ return lines.join("\n");
871
+ };
872
+ //#endregion
561
873
  //#region src/cli.ts
562
874
  var isRecord$2 = (value) => typeof value === "object" && value !== null;
563
875
  var isPackageManager = (value) => value === "npm" || value === "pnpm" || value === "yarn";
@@ -620,6 +932,35 @@ var getDefaultGithubUsername = async () => {
620
932
  return "";
621
933
  }
622
934
  };
935
+ var getMissingProjectConfigMessage$1 = (pkgPath) => [
936
+ `No "create-template-project" configuration found in ${pkgPath}.`,
937
+ "The update command only runs after a project has been adopted by this tool.",
938
+ "If this project matches a template, run \"create-template-project doctor --template <type> --directory <dir>\" to validate it, then \"create-template-project adopt --template <type> --directory <dir>\" to add the marker."
939
+ ].join(" ");
940
+ var parseTemplateOption = (template) => {
941
+ const templateInput = stripQuotes(template);
942
+ const templateResult = TemplateTypeSchema.safeParse(templateInput);
943
+ if (!templateResult.success) {
944
+ p.log.error(`Invalid template type: ${template}. Must be one of: cli, web-vanilla, web-widget, web-app, web-fullstack`);
945
+ process.exit(1);
946
+ }
947
+ return templateResult.data;
948
+ };
949
+ var adoptProject = async (directory, template) => {
950
+ const pkgPath = path.join(directory, "package.json");
951
+ const parsed = JSON.parse(await fs.readFile(pkgPath, "utf8"));
952
+ if (!isRecord$2(parsed)) {
953
+ p.log.error(`Failed to adopt project: ${pkgPath} does not contain a JSON object.`);
954
+ process.exit(1);
955
+ }
956
+ await fs.writeFile(pkgPath, `${JSON.stringify({
957
+ ...parsed,
958
+ "create-template-project": {
959
+ ...isRecord$2(parsed["create-template-project"]) ? parsed["create-template-project"] : {},
960
+ template
961
+ }
962
+ }, null, " ")}\n`);
963
+ };
623
964
  var debug$2 = debugLib("create-template-project:cli");
624
965
  var parseArgs = async () => {
625
966
  debug$2("Parsing CLI arguments: %O", process.argv);
@@ -632,27 +973,22 @@ var parseArgs = async () => {
632
973
  process.env.DEBUG = "create-template-project:*";
633
974
  debugLib.enable("create-template-project:*");
634
975
  }).addHelpText("after", `
635
- Commands:
636
- create - Create a new project from a template.
637
- update - Update an existing project from its template.
638
- interactive - Start interactive project configuration.
639
- info - Show detailed information about available templates and components.
640
-
641
976
  Templates:
642
977
  cli - Node.js CLI application with commander and cli-progress.
643
978
  web-vanilla - Standalone web page (modern HTML/JS).
979
+ web-widget - Publishable native TypeScript widget with demo, docs, and npm build.
644
980
  web-app - React application with MUI and TanStack Query.
645
981
  web-fullstack - Full-stack monorepo with Express server and React/MUI client.
646
982
  `);
647
983
  let commandResult;
648
- program.command("info").description("Show detailed information about available templates and their components").option("-t, --template <type>", "Template type (cli, web-vanilla, web-app, web-fullstack)").action((opts) => {
984
+ program.command("info").description("Show detailed information about available templates and their components").option("-t, --template <type>", "Template type (cli, web-vanilla, web-widget, web-app, web-fullstack)").action((opts) => {
649
985
  debug$2("Executing \"info\" command with options: %O", opts);
650
986
  p.intro("Template Information");
651
987
  if (opts.template !== void 0 && opts.template.length > 0) {
652
988
  const template = stripQuotes(opts.template);
653
989
  const typeResult = TemplateTypeSchema.safeParse(template);
654
990
  if (!typeResult.success) {
655
- p.log.error(`Invalid template type: ${opts.template}. Must be one of: cli, web-vanilla, web-app, web-fullstack`);
991
+ p.log.error(`Invalid template type: ${opts.template}. Must be one of: cli, web-vanilla, web-widget, web-app, web-fullstack`);
656
992
  process.exit(1);
657
993
  }
658
994
  const info = getTemplateInfo(typeResult.data);
@@ -674,12 +1010,49 @@ Templates:
674
1010
  p.outro("Use \"create\" to scaffold a new project.");
675
1011
  process.exit(0);
676
1012
  });
677
- program.command("create").description("Create a new project from a template").option("-t, --template <type>", "Template type (cli, web-vanilla, web-app, web-fullstack)").option("-n, --name <name>", "Project name").option("--description <description>", "Project description").option("-k, --keywords <keywords>", "Project keywords (comma separated)").option("-a, --author <author>", "Author name (defaults to 'git config user.name')").option("--github-username <username>", "GitHub username (defaults to 'git config github.user')").option("-p, --package-manager <pm>", "Package manager (npm, pnpm, yarn)", "pnpm").option("--create-github-repository", "Create GitHub repository and push initial commit").requiredOption("--path <path>", "Output directory").option("--build", "Run the CI script (lint, build, test) after scaffolding", false).option("--no-progress", "Do not show progress indicators").action(async (opts) => {
1013
+ program.command("doctor").description("Check whether an existing project can be adopted for template updates").requiredOption("-t, --template <type>", "Template type (cli, web-vanilla, web-widget, web-app, web-fullstack)").option("-d, --directory <path>", "Project directory", ".").action(async (opts) => {
1014
+ debug$2("Executing \"doctor\" command with options: %O", opts);
1015
+ const template = parseTemplateOption(opts.template);
1016
+ const directory = path.resolve(opts.directory);
1017
+ const report = await validateProjectCompatibility(directory, template);
1018
+ p.note(formatCompatibilityReport(report), "Compatibility Check");
1019
+ if (!report.passed) {
1020
+ p.log.error("Project is not compatible enough to adopt safely. Fix the failed checks and run doctor again.");
1021
+ process.exit(1);
1022
+ }
1023
+ p.log.success(`Project is compatible. Run "create-template-project adopt --template ${template} --directory ${directory}" to add the create-template-project marker.`);
1024
+ process.exit(0);
1025
+ });
1026
+ program.command("adopt").description("Validate an existing project and add the create-template-project update marker").requiredOption("-t, --template <type>", "Template type (cli, web-vanilla, web-widget, web-app, web-fullstack)").option("-d, --directory <path>", "Project directory", ".").option("--yes", "Adopt without confirmation", false).action(async (opts) => {
1027
+ debug$2("Executing \"adopt\" command with options: %O", opts);
1028
+ const template = parseTemplateOption(opts.template);
1029
+ const directory = path.resolve(opts.directory);
1030
+ const report = await validateProjectCompatibility(directory, template);
1031
+ p.note(formatCompatibilityReport(report), "Compatibility Check");
1032
+ if (!report.passed) {
1033
+ p.log.error("Project is not compatible enough to adopt safely. Fix the failed checks and run doctor again.");
1034
+ process.exit(1);
1035
+ }
1036
+ if (opts.yes !== true) {
1037
+ const confirmed = await p.confirm({
1038
+ message: `Add create-template-project marker for template "${template}" to ${path.join(directory, "package.json")}?`,
1039
+ initialValue: false
1040
+ });
1041
+ if (p.isCancel(confirmed) || !confirmed) {
1042
+ p.cancel("Adoption cancelled.");
1043
+ process.exit(0);
1044
+ }
1045
+ }
1046
+ await adoptProject(directory, template);
1047
+ p.log.success(`Project adopted as ${template}. You can now run "create-template-project update --directory ${directory}".`);
1048
+ process.exit(0);
1049
+ });
1050
+ program.command("create").description("Create a new project from a template").option("-t, --template <type>", "Template type (cli, web-vanilla, web-widget, web-app, web-fullstack)").option("-n, --name <name>", "Project name").option("--description <description>", "Project description").option("-k, --keywords <keywords>", "Project keywords (comma separated)").option("-a, --author <author>", "Author name (defaults to 'git config user.name')").option("--github-username <username>", "GitHub username (defaults to 'git config github.user')").option("-p, --package-manager <pm>", "Package manager (npm, pnpm, yarn)", "pnpm").option("--create-github-repository", "Create GitHub repository and push initial commit").requiredOption("--path <path>", "Output directory").option("--build", "Run the CI script (lint, build, test) after scaffolding", false).option("--no-progress", "Do not show progress indicators").action(async (opts) => {
678
1051
  debug$2("Executing \"create\" command with options: %O", opts);
679
1052
  const templateInput = stripQuotes(opts.template);
680
1053
  const templateResult = TemplateTypeSchema.safeParse(templateInput);
681
1054
  if (!templateResult.success) {
682
- p.log.error(`Invalid template type: ${opts.template}. Must be one of: cli, web-vanilla, web-app, web-fullstack`);
1055
+ p.log.error(`Invalid template type: ${opts.template}. Must be one of: cli, web-vanilla, web-widget, web-app, web-fullstack`);
683
1056
  process.exit(1);
684
1057
  }
685
1058
  commandResult = {
@@ -708,7 +1081,7 @@ Restrictions & Behavior:
708
1081
  - package.json: Dependencies and scripts are merged. Existing versions are preserved unless they are missing.
709
1082
  - Merging: For non-seed files, the tool attempts to merge template changes. If a conflict occurs, it will be marked with standard git conflict markers.
710
1083
  - Confirmation: The command will show a summary of proposed changes and lets you preview diffs before applying.
711
- `).option("-t, --template <type>", "Template type (cli, web-vanilla, web-app, web-fullstack)").option("--description <description>", "Project description").option("-k, --keywords <keywords>", "Project keywords (comma separated)").option("-a, --author <author>", "Author name (defaults to 'git config user.name')").option("--github-username <username>", "GitHub username (defaults to 'git config github.user')").option("-p, --package-manager <pm>", "Package manager (npm, pnpm, yarn)", "pnpm").option("--create-github-repository", "Create GitHub repository and push initial commit").option("-d, --directory <path>", "Output directory", ".").option("--build", "Run the CI script (lint, build, test) after updating", false).option("--dev", "Run the dev server after scaffolding", false).option("--open", "Open the browser after scaffolding", false).option("--no-progress", "Do not show progress indicators").action(async (opts) => {
1084
+ `).option("-t, --template <type>", "Template type (cli, web-vanilla, web-widget, web-app, web-fullstack)").option("--description <description>", "Project description").option("-k, --keywords <keywords>", "Project keywords (comma separated)").option("-a, --author <author>", "Author name (defaults to 'git config user.name')").option("--github-username <username>", "GitHub username (defaults to 'git config github.user')").option("-p, --package-manager <pm>", "Package manager (npm, pnpm, yarn)", "pnpm").option("--create-github-repository", "Create GitHub repository and push initial commit").option("-d, --directory <path>", "Output directory", ".").option("--build", "Run the CI script (lint, build, test) after updating", false).option("--dev", "Run the dev server after scaffolding", false).option("--open", "Open the browser after scaffolding", false).option("--no-progress", "Do not show progress indicators").action(async (opts) => {
712
1085
  debug$2("Executing \"update\" command with options: %O", opts);
713
1086
  const directory = path.resolve(opts.directory);
714
1087
  const pkgPath = path.join(directory, "package.json");
@@ -730,19 +1103,11 @@ Restrictions & Behavior:
730
1103
  }
731
1104
  const projectConfig = pkg["create-template-project"];
732
1105
  if (projectConfig === void 0 || projectConfig.template === void 0) {
733
- p.log.error(`No "create-template-project" configuration found in ${pkgPath}. The update command can only be used on projects created with this tool.`);
1106
+ p.log.error(getMissingProjectConfigMessage$1(pkgPath));
734
1107
  process.exit(1);
735
1108
  }
736
1109
  let template = projectConfig.template;
737
- if (opts.template !== void 0) {
738
- const templateInput = stripQuotes(opts.template);
739
- const templateResult = TemplateTypeSchema.safeParse(templateInput);
740
- if (!templateResult.success) {
741
- p.log.error(`Invalid template type: ${opts.template}. Must be one of: cli, web-vanilla, web-app, web-fullstack`);
742
- process.exit(1);
743
- }
744
- template = templateResult.data;
745
- }
1110
+ if (opts.template !== void 0) template = parseTemplateOption(opts.template);
746
1111
  commandResult = {
747
1112
  ...opts,
748
1113
  update: true,
@@ -859,7 +1224,7 @@ Restrictions & Behavior:
859
1224
  process.exit(0);
860
1225
  }
861
1226
  if (!existingConfig.template) {
862
- p.log.error(`No "create-template-project" configuration found in ${pkgPath}. The update command can only be used on projects created with this tool.`);
1227
+ p.log.error(getMissingProjectConfigMessage$1(pkgPath));
863
1228
  process.exit(1);
864
1229
  }
865
1230
  update = true;
@@ -879,6 +1244,10 @@ Restrictions & Behavior:
879
1244
  label: "Web-Vanilla (Standalone)",
880
1245
  value: "web-vanilla"
881
1246
  },
1247
+ {
1248
+ label: "Web-Widget (Publishable)",
1249
+ value: "web-widget"
1250
+ },
882
1251
  {
883
1252
  label: "Web-App (React + MUI)",
884
1253
  value: "web-app"
@@ -1070,12 +1439,62 @@ var isFileRequired = (relativePath, type) => {
1070
1439
  if (relativePath === "vitest.config.ts") return ![
1071
1440
  "cli",
1072
1441
  "web-vanilla",
1442
+ "web-widget",
1073
1443
  "web-app",
1074
1444
  "web-fullstack"
1075
1445
  ].includes(type);
1076
1446
  return true;
1077
1447
  };
1078
- var isCustomConfigFile = (relativePath) => relativePath === "oxlint.config.ts" || relativePath === "oxfmt.config.ts";
1448
+ var isCustomConfigFile = (relativePath) => relativePath === "oxlint.config.ts" || relativePath === "oxfmt.config.ts" || relativePath === "stylelint.config.js";
1449
+ var toPosixPath = (filePath) => filePath.split(path.sep).join("/");
1450
+ var isOptionalCapabilityFile = (filePath) => {
1451
+ const normalizedPath = toPosixPath(filePath);
1452
+ return normalizedPath.startsWith(".github/workflows/") && normalizedPath !== ".github/workflows/ci.yml" && normalizedPath !== ".github/workflows/node.js.yml";
1453
+ };
1454
+ var isStarterFile = (filePath) => {
1455
+ const normalizedPath = toPosixPath(filePath);
1456
+ return isSeedFile(normalizedPath) || normalizedPath.startsWith("tests/e2e/") || normalizedPath.startsWith("src/demo/") || normalizedPath.endsWith(".test.ts") || normalizedPath.endsWith(".test.tsx") || normalizedPath.endsWith(".spec.ts") || normalizedPath.endsWith(".spec.tsx");
1457
+ };
1458
+ var getAddReason = (filePath) => {
1459
+ const normalizedPath = toPosixPath(filePath);
1460
+ if (normalizedPath === "package.json") return "Initial project metadata, dependencies, and scripts";
1461
+ if (normalizedPath === ".npmrc") return "Package manager configuration required by the template";
1462
+ if (normalizedPath === "pnpm-workspace.yaml") return "Workspace configuration required by pnpm";
1463
+ if (normalizedPath === ".github/workflows/ci.yml") return "Continuous integration workflow required by the base template";
1464
+ if (normalizedPath.startsWith(".github/workflows/")) return "GitHub workflow provided by the template";
1465
+ if (normalizedPath.includes("lint") || normalizedPath.includes("oxc") || normalizedPath.includes("oxfmt") || normalizedPath.includes("stylelint")) return "Linting and formatting configuration required by the template";
1466
+ if (normalizedPath.includes("tsconfig") || normalizedPath.includes("vite") || normalizedPath.includes("tsdown") || normalizedPath.includes("playwright") || normalizedPath.includes("typedoc")) return "Build, test, or documentation tooling configuration required by the template";
1467
+ return "Template tooling file required for this project type";
1468
+ };
1469
+ var getModifyReason = (filePath) => {
1470
+ const normalizedPath = toPosixPath(filePath);
1471
+ if (normalizedPath === "package.json") return "Update dependencies, scripts, and template metadata";
1472
+ if (normalizedPath === "pnpm-workspace.yaml") return "Update workspace configuration for pnpm";
1473
+ if (normalizedPath === ".github/workflows/ci.yml" || normalizedPath === ".github/workflows/node.js.yml") return "Update continuous integration workflow to match the base template";
1474
+ if (normalizedPath.startsWith(".github/workflows/")) return "Update GitHub workflow to match the template";
1475
+ if (normalizedPath.includes("lint") || normalizedPath.includes("oxc") || normalizedPath.includes("oxfmt") || normalizedPath.includes("stylelint")) return "Update linting and formatting configuration";
1476
+ if (normalizedPath.includes("tsconfig") || normalizedPath.includes("vite") || normalizedPath.includes("tsdown") || normalizedPath.includes("playwright") || normalizedPath.includes("typedoc")) return "Update build, test, or documentation tooling configuration";
1477
+ return "Update template-managed tooling or configuration";
1478
+ };
1479
+ var getDeleteReason = (filePath) => {
1480
+ if (toPosixPath(filePath) === "vitest.config.ts") return "File no longer required because this template uses Vite/Vitest configuration";
1481
+ return "File no longer required for this template type";
1482
+ };
1483
+ var getUpdateFilePolicy = (filePath) => {
1484
+ if (isOptionalCapabilityFile(filePath)) return {
1485
+ shouldAddDuringUpdate: false,
1486
+ reason: "Optional capability file - available but skipped during update"
1487
+ };
1488
+ if (isStarterFile(filePath)) return {
1489
+ shouldAddDuringUpdate: false,
1490
+ reason: "Starter/example project file - available but skipped during update"
1491
+ };
1492
+ return {
1493
+ shouldAddDuringUpdate: true,
1494
+ reason: getAddReason(filePath)
1495
+ };
1496
+ };
1497
+ var formatActionSummary = (actions) => actions.map((a) => ` ${a.type.padEnd(8)} ${a.path}${a.reason === void 0 ? "" : ` - ${a.reason}`}`).join("\n");
1079
1498
  var buildSimpleUnifiedDiff = (filePath, before, after) => {
1080
1499
  const beforeLines = before.split("\n");
1081
1500
  const afterLines = after.split("\n");
@@ -1099,7 +1518,12 @@ var buildSimpleUnifiedDiff = (filePath, before, after) => {
1099
1518
  };
1100
1519
  var getTemplateArchitectureSection;
1101
1520
  var generateGeneratedMd;
1102
- var isTemplateType = (value) => value === "cli" || value === "web-vanilla" || value === "web-app" || value === "web-fullstack";
1521
+ var isTemplateType = (value) => value === "cli" || value === "web-vanilla" || value === "web-widget" || value === "web-app" || value === "web-fullstack";
1522
+ var getMissingProjectConfigMessage = (pkgPath) => [
1523
+ `No "create-template-project" configuration found in ${pkgPath}.`,
1524
+ "The update command only runs after a project has been adopted by this tool.",
1525
+ "Run \"create-template-project doctor --template <type> --directory <dir>\" to validate the structure, then \"create-template-project adopt --template <type> --directory <dir>\" to add the marker."
1526
+ ].join(" ");
1103
1527
  var isRecord$1 = (value) => typeof value === "object" && value !== null;
1104
1528
  var parseStringRecord = (value) => {
1105
1529
  if (!isRecord$1(value)) return {};
@@ -1131,9 +1555,38 @@ var parseExistingProjectPackage = (raw) => {
1131
1555
  scripts: parseStringRecord(parsed.scripts),
1132
1556
  dependencies: parseStringRecord(parsed.dependencies),
1133
1557
  devDependencies: parseStringRecord(parsed.devDependencies),
1558
+ peerDependencies: parseStringRecord(parsed.peerDependencies),
1134
1559
  "create-template-project": typeof template === "string" && isTemplateType(template) ? { template } : void 0
1135
1560
  };
1136
1561
  };
1562
+ var parseExactVersion = (version) => {
1563
+ const match = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:[-+].*)?$/u.exec(version);
1564
+ if (match?.groups === void 0) return;
1565
+ return [
1566
+ Number(match.groups.major),
1567
+ Number(match.groups.minor),
1568
+ Number(match.groups.patch)
1569
+ ];
1570
+ };
1571
+ var isGreaterOrEqualVersion = (current, next) => {
1572
+ const currentParts = parseExactVersion(current);
1573
+ const nextParts = parseExactVersion(next);
1574
+ if (currentParts === void 0 || nextParts === void 0) return false;
1575
+ for (const [index, currentPart] of currentParts.entries()) {
1576
+ const nextPart = nextParts[index];
1577
+ if (currentPart > nextPart) return true;
1578
+ if (currentPart < nextPart) return false;
1579
+ }
1580
+ return true;
1581
+ };
1582
+ var mergePackageRecord = (target, source, preserveExistingVersions) => {
1583
+ const merged = { ...target };
1584
+ for (const [name, version] of Object.entries(source)) {
1585
+ if (preserveExistingVersions && Object.hasOwn(merged, name) && isGreaterOrEqualVersion(merged[name], version)) continue;
1586
+ merged[name] = version;
1587
+ }
1588
+ return merged;
1589
+ };
1137
1590
  var parseTemplatePackageJson = (raw) => {
1138
1591
  const parsed = JSON.parse(raw);
1139
1592
  if (!isRecord$1(parsed)) return {};
@@ -1180,16 +1633,19 @@ var generateProject = async (opts) => {
1180
1633
  const depConfig = parseDependencyConfig(await fs.readFile(depConfigPath, "utf8"));
1181
1634
  const addedDeps = [];
1182
1635
  const resolveDeps = (deps = {}) => {
1183
- for (const dep of Object.keys(deps)) if (Object.hasOwn(depConfig.dependencies, dep)) {
1184
- const config = depConfig.dependencies[dep];
1185
- deps[dep] = config.version;
1186
- addedDeps.push({
1187
- name: dep,
1188
- description: config.description
1189
- });
1190
- } else {
1191
- log.warn(`Dependency "${dep}" not found in central configuration. Using empty version.`);
1192
- debug$1(`Dependency "${dep}" missing in config`);
1636
+ for (const [dep, version] of Object.entries(deps)) {
1637
+ if (version.length > 0 && version !== dep) continue;
1638
+ if (Object.hasOwn(depConfig.dependencies, dep)) {
1639
+ const config = depConfig.dependencies[dep];
1640
+ deps[dep] = config.version;
1641
+ addedDeps.push({
1642
+ name: dep,
1643
+ description: config.description
1644
+ });
1645
+ } else {
1646
+ log.warn(`Dependency "${dep}" not found in central configuration. Using empty version.`);
1647
+ debug$1(`Dependency "${dep}" missing in config`);
1648
+ }
1193
1649
  }
1194
1650
  };
1195
1651
  let finalPkg = {
@@ -1215,18 +1671,30 @@ var generateProject = async (opts) => {
1215
1671
  const pkgPath = path.join(projectDir, "package.json");
1216
1672
  if (isUpdate && await pathExists(pkgPath)) {
1217
1673
  debug$1("Loading existing package.json for update");
1674
+ const defaultPkg = finalPkg;
1218
1675
  const existingPkg = parseExistingProjectPackage(await fs.readFile(pkgPath, "utf8"));
1219
- if (!existingPkg["create-template-project"]) throw new Error(`No "create-template-project" configuration found in ${pkgPath}. The update command can only be used on projects created with this tool.`);
1676
+ if (!existingPkg["create-template-project"]) throw new Error(getMissingProjectConfigMessage(pkgPath));
1220
1677
  finalPkg = {
1221
- ...finalPkg,
1222
1678
  ...existingPkg,
1679
+ name: existingPkg.name ?? defaultPkg.name,
1680
+ version: existingPkg.version ?? defaultPkg.version,
1681
+ private: existingPkg.private,
1682
+ description: existingPkg.description ?? defaultPkg.description,
1683
+ keywords: existingPkg.keywords ?? defaultPkg.keywords,
1684
+ homepage: existingPkg.homepage ?? defaultPkg.homepage,
1685
+ bugs: existingPkg.bugs ?? defaultPkg.bugs,
1686
+ license: existingPkg.license ?? defaultPkg.license,
1687
+ author: existingPkg.author ?? defaultPkg.author,
1688
+ repository: existingPkg.repository ?? defaultPkg.repository,
1689
+ type: existingPkg.type ?? defaultPkg.type,
1223
1690
  "create-template-project": {
1224
1691
  ...existingPkg["create-template-project"],
1225
1692
  template: type
1226
1693
  },
1227
1694
  scripts: { ...existingPkg.scripts },
1228
1695
  dependencies: { ...existingPkg.dependencies },
1229
- devDependencies: { ...existingPkg.devDependencies }
1696
+ devDependencies: { ...existingPkg.devDependencies },
1697
+ peerDependencies: { ...existingPkg.peerDependencies }
1230
1698
  };
1231
1699
  debug$1("Loaded existing package.json: %O", finalPkg);
1232
1700
  }
@@ -1243,14 +1711,17 @@ var generateProject = async (opts) => {
1243
1711
  resolveDeps(templateDeps);
1244
1712
  resolveDeps(templateDevDeps);
1245
1713
  Object.assign(finalPkg.scripts, t.scripts);
1246
- Object.assign(finalPkg.dependencies, templateDeps);
1247
- Object.assign(finalPkg.devDependencies, templateDevDeps);
1714
+ finalPkg.dependencies = mergePackageRecord(finalPkg.dependencies, templateDeps, isUpdate);
1715
+ finalPkg.devDependencies = mergePackageRecord(finalPkg.devDependencies, templateDevDeps, isUpdate);
1248
1716
  if (t.workspaces !== void 0) finalPkg.workspaces = t.workspaces;
1249
1717
  const pkgPart = templatePackageParts[index];
1250
1718
  if (pkgPart !== void 0) {
1251
1719
  resolveDeps(pkgPart.dependencies);
1252
1720
  resolveDeps(pkgPart.devDependencies);
1253
- mergePackageJson(finalPkg, pkgPart);
1721
+ mergePackageJson(finalPkg, pkgPart, {
1722
+ preserveExistingPackageFields: isUpdate,
1723
+ preserveExistingDependencyVersions: isUpdate
1724
+ });
1254
1725
  }
1255
1726
  }
1256
1727
  const actions = [];
@@ -1274,13 +1745,22 @@ var generateProject = async (opts) => {
1274
1745
  }
1275
1746
  if (!isFileRequired(relativePath, type)) {
1276
1747
  if (isUpdate && await pathExists(targetPath)) {
1748
+ const existingContent = await fs.readFile(targetPath, "utf8");
1749
+ plannedDiffs.push({
1750
+ path: relativePath,
1751
+ before: existingContent,
1752
+ after: ""
1753
+ });
1277
1754
  actions.push({
1278
1755
  type: "DELETE",
1279
1756
  path: relativePath,
1280
- reason: "File no longer required for this template type"
1757
+ reason: getDeleteReason(relativePath)
1281
1758
  });
1282
- pendingOperations.push(async () => {
1283
- await fs.rm(targetPath, { force: true });
1759
+ pendingOperations.push({
1760
+ path: relativePath,
1761
+ run: async () => {
1762
+ await fs.rm(targetPath, { force: true });
1763
+ }
1284
1764
  });
1285
1765
  }
1286
1766
  continue;
@@ -1289,7 +1769,7 @@ var generateProject = async (opts) => {
1289
1769
  relativePath = relativePath.slice(1);
1290
1770
  targetPath = path.join(projectDir, relativePath);
1291
1771
  }
1292
- if (isUpdate && (relativePath === "oxlint.config.ts" || relativePath === "oxfmt.config.ts")) {
1772
+ if (isUpdate && isCustomConfigFile(relativePath)) {
1293
1773
  actions.push({
1294
1774
  type: "SKIP",
1295
1775
  path: relativePath,
@@ -1313,36 +1793,50 @@ var generateProject = async (opts) => {
1313
1793
  const action = {
1314
1794
  type: "MODIFY",
1315
1795
  path: finalRelativePath,
1316
- reason: "Template tooling or configuration update"
1796
+ reason: getModifyReason(finalRelativePath)
1317
1797
  };
1318
1798
  actions.push(action);
1319
- pendingOperations.push(async () => {
1320
- if (finalRelativePath === "oxc.config.ts") {
1321
- await fs.writeFile(finalTargetPath, content);
1322
- action.type = "UPDATED";
1323
- action.reason = "Default config was updated to the latest template version";
1324
- log.info(`✔ Updated: ${finalRelativePath}`);
1325
- } else {
1326
- const result = await mergeFile(finalTargetPath, existingContent, content, log);
1327
- if (result === "merged") {
1328
- action.type = "MERGE";
1329
- action.reason = "Merged template updates with your manual changes";
1330
- action.recommendedAction = "Review changes for correct integration";
1331
- log.info(`ℹ Merged: ${finalRelativePath}`);
1332
- } else if (result === "conflict") {
1333
- action.type = "CONFLICT";
1334
- action.reason = "Conflicting changes between template and your code";
1335
- action.recommendedAction = "Resolve git conflict markers in this file";
1336
- log.warn(`⚠ Conflict: ${finalRelativePath}`);
1337
- } else if (result === "updated") {
1799
+ pendingOperations.push({
1800
+ path: finalRelativePath,
1801
+ run: async () => {
1802
+ if (finalRelativePath === "oxc.config.ts") {
1803
+ await fs.writeFile(finalTargetPath, content);
1338
1804
  action.type = "UPDATED";
1339
- action.reason = "File was updated to the latest template version";
1805
+ action.reason = "Default config was updated to the latest template version";
1340
1806
  log.info(`✔ Updated: ${finalRelativePath}`);
1807
+ } else {
1808
+ const result = await mergeFile(finalTargetPath, existingContent, content, log);
1809
+ if (result === "merged") {
1810
+ action.type = "MERGE";
1811
+ action.reason = "Merged template updates with your manual changes";
1812
+ action.recommendedAction = "Review changes for correct integration";
1813
+ log.info(`ℹ Merged: ${finalRelativePath}`);
1814
+ } else if (result === "conflict") {
1815
+ action.type = "CONFLICT";
1816
+ action.reason = "Conflicting changes between template and your code";
1817
+ action.recommendedAction = "Resolve git conflict markers in this file";
1818
+ log.warn(`⚠ Conflict: ${finalRelativePath}`);
1819
+ } else if (result === "updated") {
1820
+ action.type = "UPDATED";
1821
+ action.reason = "File was updated to the latest template version";
1822
+ log.info(`✔ Updated: ${finalRelativePath}`);
1823
+ }
1341
1824
  }
1342
1825
  }
1343
1826
  });
1344
1827
  }
1345
1828
  } else if (!exists) {
1829
+ if (isUpdate) {
1830
+ const policy = getUpdateFilePolicy(finalRelativePath);
1831
+ if (!policy.shouldAddDuringUpdate) {
1832
+ actions.push({
1833
+ type: "SKIP",
1834
+ path: finalRelativePath,
1835
+ reason: policy.reason
1836
+ });
1837
+ continue;
1838
+ }
1839
+ }
1346
1840
  plannedDiffs.push({
1347
1841
  path: finalRelativePath,
1348
1842
  before: "",
@@ -1351,13 +1845,16 @@ var generateProject = async (opts) => {
1351
1845
  actions.push({
1352
1846
  type: "ADD",
1353
1847
  path: finalRelativePath,
1354
- reason: "New template file added"
1848
+ reason: getAddReason(finalRelativePath)
1355
1849
  });
1356
- pendingOperations.push(async () => {
1357
- await fs.mkdir(path.dirname(finalTargetPath), { recursive: true });
1358
- await fs.writeFile(finalTargetPath, content);
1359
- const normalizedPath = finalRelativePath.split(path.sep).join("/");
1360
- if (normalizedPath.startsWith(".husky/") && !normalizedPath.endsWith("/")) await fs.chmod(finalTargetPath, 493);
1850
+ pendingOperations.push({
1851
+ path: finalRelativePath,
1852
+ run: async () => {
1853
+ await fs.mkdir(path.dirname(finalTargetPath), { recursive: true });
1854
+ await fs.writeFile(finalTargetPath, content);
1855
+ const normalizedPath = finalRelativePath.split(path.sep).join("/");
1856
+ if (normalizedPath.startsWith(".husky/") && !normalizedPath.endsWith("/")) await fs.chmod(finalTargetPath, 493);
1857
+ }
1361
1858
  });
1362
1859
  }
1363
1860
  }
@@ -1382,13 +1879,22 @@ var generateProject = async (opts) => {
1382
1879
  }
1383
1880
  if (!isFileRequired(file.path, type)) {
1384
1881
  if (isUpdate && await pathExists(targetPath)) {
1882
+ const existingContent = await fs.readFile(targetPath, "utf8");
1883
+ plannedDiffs.push({
1884
+ path: file.path,
1885
+ before: existingContent,
1886
+ after: ""
1887
+ });
1385
1888
  actions.push({
1386
1889
  type: "DELETE",
1387
1890
  path: file.path,
1388
- reason: "File no longer required for this template type"
1891
+ reason: getDeleteReason(file.path)
1389
1892
  });
1390
- pendingOperations.push(async () => {
1391
- await fs.rm(targetPath, { force: true });
1893
+ pendingOperations.push({
1894
+ path: file.path,
1895
+ run: async () => {
1896
+ await fs.rm(targetPath, { force: true });
1897
+ }
1392
1898
  });
1393
1899
  }
1394
1900
  continue;
@@ -1407,36 +1913,50 @@ var generateProject = async (opts) => {
1407
1913
  const action = {
1408
1914
  type: "MODIFY",
1409
1915
  path: file.path,
1410
- reason: "Template configuration update"
1916
+ reason: getModifyReason(file.path)
1411
1917
  };
1412
1918
  actions.push(action);
1413
- pendingOperations.push(async () => {
1414
- if (file.path === "oxc.config.ts") {
1415
- await fs.writeFile(targetPath, content);
1416
- action.type = "UPDATED";
1417
- action.reason = "Default config was updated to the latest template version";
1418
- log.info(`✔ Updated: ${file.path}`);
1419
- } else {
1420
- const result = await mergeFile(targetPath, existingContent, content, log);
1421
- if (result === "merged") {
1422
- action.type = "MERGE";
1423
- action.reason = "Merged template updates with your manual changes";
1424
- action.recommendedAction = "Review changes for correct integration";
1425
- log.info(`ℹ Merged: ${file.path}`);
1426
- } else if (result === "conflict") {
1427
- action.type = "CONFLICT";
1428
- action.reason = "Conflicting changes between template and your code";
1429
- action.recommendedAction = "Resolve git conflict markers in this file";
1430
- log.warn(`⚠ Conflict: ${file.path}`);
1431
- } else if (result === "updated") {
1919
+ pendingOperations.push({
1920
+ path: file.path,
1921
+ run: async () => {
1922
+ if (file.path === "oxc.config.ts") {
1923
+ await fs.writeFile(targetPath, content);
1432
1924
  action.type = "UPDATED";
1433
- action.reason = "File was updated to the latest template version";
1925
+ action.reason = "Default config was updated to the latest template version";
1434
1926
  log.info(`✔ Updated: ${file.path}`);
1927
+ } else {
1928
+ const result = await mergeFile(targetPath, existingContent, content, log);
1929
+ if (result === "merged") {
1930
+ action.type = "MERGE";
1931
+ action.reason = "Merged template updates with your manual changes";
1932
+ action.recommendedAction = "Review changes for correct integration";
1933
+ log.info(`ℹ Merged: ${file.path}`);
1934
+ } else if (result === "conflict") {
1935
+ action.type = "CONFLICT";
1936
+ action.reason = "Conflicting changes between template and your code";
1937
+ action.recommendedAction = "Resolve git conflict markers in this file";
1938
+ log.warn(`⚠ Conflict: ${file.path}`);
1939
+ } else if (result === "updated") {
1940
+ action.type = "UPDATED";
1941
+ action.reason = "File was updated to the latest template version";
1942
+ log.info(`✔ Updated: ${file.path}`);
1943
+ }
1435
1944
  }
1436
1945
  }
1437
1946
  });
1438
1947
  }
1439
1948
  } else if (!exists) {
1949
+ if (isUpdate) {
1950
+ const policy = getUpdateFilePolicy(file.path);
1951
+ if (!policy.shouldAddDuringUpdate) {
1952
+ actions.push({
1953
+ type: "SKIP",
1954
+ path: file.path,
1955
+ reason: policy.reason
1956
+ });
1957
+ continue;
1958
+ }
1959
+ }
1440
1960
  plannedDiffs.push({
1441
1961
  path: file.path,
1442
1962
  before: "",
@@ -1445,11 +1965,14 @@ var generateProject = async (opts) => {
1445
1965
  actions.push({
1446
1966
  type: "ADD",
1447
1967
  path: file.path,
1448
- reason: "New template file added"
1968
+ reason: getAddReason(file.path)
1449
1969
  });
1450
- pendingOperations.push(async () => {
1451
- await fs.mkdir(path.dirname(targetPath), { recursive: true });
1452
- await fs.writeFile(targetPath, content);
1970
+ pendingOperations.push({
1971
+ path: file.path,
1972
+ run: async () => {
1973
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
1974
+ await fs.writeFile(targetPath, content);
1975
+ }
1453
1976
  });
1454
1977
  }
1455
1978
  }
@@ -1473,10 +1996,13 @@ var generateProject = async (opts) => {
1473
1996
  actions.push({
1474
1997
  type: "ADD",
1475
1998
  path: ".npmrc",
1476
- reason: "Initial package manager configuration"
1999
+ reason: getAddReason(".npmrc")
1477
2000
  });
1478
- pendingOperations.push(async () => {
1479
- await fs.writeFile(npmrcPath, npmrcContent);
2001
+ pendingOperations.push({
2002
+ path: ".npmrc",
2003
+ run: async () => {
2004
+ await fs.writeFile(npmrcPath, npmrcContent);
2005
+ }
1480
2006
  });
1481
2007
  }
1482
2008
  if (pm === "pnpm" && finalPkg.workspaces) {
@@ -1496,10 +2022,13 @@ var generateProject = async (opts) => {
1496
2022
  actions.push({
1497
2023
  type: workspaceExists ? "MODIFY" : "ADD",
1498
2024
  path: "pnpm-workspace.yaml",
1499
- reason: "Updated workspace configuration for pnpm"
2025
+ reason: workspaceExists ? getModifyReason("pnpm-workspace.yaml") : getAddReason("pnpm-workspace.yaml")
1500
2026
  });
1501
- pendingOperations.push(async () => {
1502
- await fs.writeFile(workspacePath, workspaceYaml);
2027
+ pendingOperations.push({
2028
+ path: "pnpm-workspace.yaml",
2029
+ run: async () => {
2030
+ await fs.writeFile(workspacePath, workspaceYaml);
2031
+ }
1503
2032
  });
1504
2033
  }
1505
2034
  delete finalPkg.workspaces;
@@ -1521,35 +2050,50 @@ var generateProject = async (opts) => {
1521
2050
  if (isUpdate) actions.push({
1522
2051
  type: "MODIFY",
1523
2052
  path: "package.json",
1524
- reason: "Updated dependencies and scripts to match latest template"
2053
+ reason: getModifyReason("package.json")
1525
2054
  });
1526
2055
  else actions.push({
1527
2056
  type: "ADD",
1528
2057
  path: "package.json",
1529
- reason: "Initial project configuration"
2058
+ reason: getAddReason("package.json")
1530
2059
  });
1531
- pendingOperations.push(async () => {
1532
- debug$1("Writing final consolidated package.json to: %s", pkgPath);
1533
- await fs.writeFile(pkgPath, newPkgContent);
2060
+ pendingOperations.push({
2061
+ path: "package.json",
2062
+ run: async () => {
2063
+ debug$1("Writing final consolidated package.json to: %s", pkgPath);
2064
+ await fs.writeFile(pkgPath, newPkgContent);
2065
+ }
1534
2066
  });
1535
2067
  }
1536
2068
  if (isUpdate && actions.length > 0 && process.env.NODE_ENV !== "test") {
1537
- const summary = actions.filter((a) => a.type !== "SKIP").map((a) => ` ${a.type.padEnd(8)} ${a.path}`).join("\n");
2069
+ const summary = formatActionSummary(actions);
1538
2070
  if (summary) {
1539
2071
  const relativeProjectDir = path.relative(process.cwd(), projectDir) || ".";
1540
2072
  p.note(summary, `Planned changes in ${relativeProjectDir}:`);
2073
+ const syncedPaths = /* @__PURE__ */ new Set();
2074
+ const syncFile = async (filePath) => {
2075
+ const operation = pendingOperations.find((candidate) => candidate.path === filePath && !syncedPaths.has(candidate.path));
2076
+ if (operation === void 0) {
2077
+ p.note("This file has no pending sync operation.", `Sync: ${filePath}`);
2078
+ return;
2079
+ }
2080
+ await operation.run();
2081
+ syncedPaths.add(filePath);
2082
+ p.log.success(`Synced ${filePath}`);
2083
+ };
1541
2084
  let confirmed = false;
1542
2085
  while (!confirmed) {
1543
- const action = await p.select({
1544
- message: "Choose next step:",
2086
+ const selectedFile = await p.select({
2087
+ message: "Choose file to inspect or sync:",
1545
2088
  options: [
2089
+ ...plannedDiffs.map((entry) => ({
2090
+ label: entry.path,
2091
+ value: entry.path,
2092
+ hint: `${syncedPaths.has(entry.path) ? "synced; " : ""}${actions.find((candidate) => candidate.path === entry.path)?.reason ?? "Planned file change"}`
2093
+ })),
1546
2094
  {
1547
- label: "Show diff",
1548
- value: "show-diff"
1549
- },
1550
- {
1551
- label: "Apply changes",
1552
- value: "apply"
2095
+ label: "Continue update",
2096
+ value: "continue-update"
1553
2097
  },
1554
2098
  {
1555
2099
  label: "Cancel update",
@@ -1557,24 +2101,57 @@ var generateProject = async (opts) => {
1557
2101
  }
1558
2102
  ]
1559
2103
  });
1560
- if (p.isCancel(action) || action === "cancel") {
2104
+ if (p.isCancel(selectedFile) || selectedFile === "cancel") {
1561
2105
  p.cancel("Update cancelled.");
1562
2106
  process.exit(0);
1563
2107
  }
1564
- if (action === "show-diff") {
1565
- for (const entry of plannedDiffs) {
1566
- const diff = buildSimpleUnifiedDiff(entry.path, entry.before, entry.after);
1567
- p.note(diff, `Diff preview: ${entry.path}`);
2108
+ if (selectedFile === "continue-update") {
2109
+ confirmed = true;
2110
+ continue;
2111
+ }
2112
+ const fileActionOptions = syncedPaths.has(selectedFile) ? [{
2113
+ label: "Show diff",
2114
+ value: "show-diff"
2115
+ }, {
2116
+ label: "Back",
2117
+ value: "back"
2118
+ }] : [
2119
+ {
2120
+ label: "Show diff",
2121
+ value: "show-diff"
2122
+ },
2123
+ {
2124
+ label: "Sync file",
2125
+ value: "sync-file"
2126
+ },
2127
+ {
2128
+ label: "Back",
2129
+ value: "back"
2130
+ }
2131
+ ];
2132
+ const fileAction = await p.select({
2133
+ message: `Choose action for ${selectedFile}:`,
2134
+ options: fileActionOptions
2135
+ });
2136
+ if (p.isCancel(fileAction) || fileAction === "back") continue;
2137
+ if (fileAction === "show-diff") {
2138
+ const selectedDiff = plannedDiffs.find((entry) => entry.path === selectedFile);
2139
+ if (selectedDiff !== void 0) {
2140
+ const diff = buildSimpleUnifiedDiff(selectedDiff.path, selectedDiff.before, selectedDiff.after);
2141
+ p.note(diff, `Diff preview: ${selectedDiff.path}`);
1568
2142
  }
1569
2143
  continue;
1570
2144
  }
1571
- confirmed = true;
2145
+ if (fileAction === "sync-file") await syncFile(selectedFile);
1572
2146
  }
2147
+ await pendingOperations.filter((operation) => !syncedPaths.has(operation.path)).reduce(async (previous, operation) => {
2148
+ await previous;
2149
+ await operation.run();
2150
+ }, Promise.resolve());
1573
2151
  } else log.info("No changes detected.");
1574
- }
1575
- await pendingOperations.reduce(async (previous, operation) => {
2152
+ } else await pendingOperations.reduce(async (previous, operation) => {
1576
2153
  await previous;
1577
- await operation();
2154
+ await operation.run();
1578
2155
  }, Promise.resolve());
1579
2156
  const states = {
1580
2157
  gitInitialized: false,
@@ -1911,6 +2488,20 @@ getTemplateArchitectureSection = (template) => {
1911
2488
  "- Add new UI logic or Web Components inside the `src/` directory.",
1912
2489
  "- Create styling (`.css` or `.scss`) and import them directly into `main.ts`."
1913
2490
  ];
2491
+ case "web-widget": return [
2492
+ "## 🏗️ Web Widget Architecture",
2493
+ "A publishable native TypeScript widget package with a Vite-powered demo site.",
2494
+ "",
2495
+ "### Source Files Generated",
2496
+ "- **`src/lib/`**: The public widget implementation, exports, and optional React adapter subpath.",
2497
+ "- **`src/styles/index.css`**: The exported widget stylesheet available through the package `./styles.css` subpath.",
2498
+ "- **`src/demo/`**: The demo application used by Vite, Playwright, and screenshot generation.",
2499
+ "",
2500
+ "### How to Enhance",
2501
+ "- Keep reusable package logic in `src/lib/` and demo-only behavior in `src/demo/`.",
2502
+ "- Update `tsdown.config.ts` and `package.json` exports together when adding public entry points.",
2503
+ "- Run `pnpm run docs:api` and `pnpm run screenshot` before publishing visible API or demo changes."
2504
+ ];
1914
2505
  case "web-app": return [
1915
2506
  "## 🏗️ Web App Architecture",
1916
2507
  "A robust React SPA configured with MUI components and TanStack Query.",