create-gtkx 1.6.0 → 2.0.0-beta.2

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
@@ -13,7 +13,7 @@
13
13
  <p align="center">
14
14
  <a href="https://www.npmjs.com/package/create-gtkx"><img src="https://img.shields.io/npm/v/create-gtkx?color=cb3837&logo=npm&label=create-gtkx" alt="npm version" /></a>
15
15
  <a href="https://www.npmjs.com/package/create-gtkx"><img src="https://img.shields.io/npm/dm/create-gtkx?color=cb3837&logo=npm&label=downloads" alt="npm downloads" /></a>
16
- <img src="https://img.shields.io/badge/node-%E2%89%A524-339933?logo=node.js&logoColor=white" alt="Node >= 24" />
16
+ <img src="https://img.shields.io/badge/node-%E2%89%A526.7-339933?logo=node.js&logoColor=white" alt="Node >= 26.7" />
17
17
  <a href="https://github.com/gtkx-org/gtkx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MPL--2.0-blue.svg" alt="License: MPL-2.0" /></a>
18
18
  <a href="https://github.com/gtkx-org/gtkx/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/gtkx-org/gtkx/ci.yml?branch=main&logo=github&label=CI" alt="CI status" /></a>
19
19
  <img src="https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white" alt="TypeScript" />
@@ -116,7 +116,7 @@ At runtime, the native Rust core calls straight into the system GTK4, Adwaita, a
116
116
 
117
117
  ## Quick start
118
118
 
119
- GTKX is Linux-only and needs Node.js 24 or later. See [Requirements](#requirements).
119
+ GTKX is Linux-only and needs Node.js 26.7 or later. See [Requirements](#requirements).
120
120
 
121
121
  Scaffold a new app with the `create-gtkx` initializer:
122
122
 
@@ -145,8 +145,8 @@ The documentation at **[gtkx.dev](https://gtkx.dev)** includes a step-by-step tu
145
145
 
146
146
  GTKX is Linux-only. You need:
147
147
 
148
- - Linux with the GTK4 (4.20 or later) and GLib development libraries, plus Adwaita (1.8 or later) once your project binds `Adw-1`
149
- - Node.js 24 or later
148
+ - Linux with the GTK4 (4.20 or later), Adwaita (1.8 or later), and GLib development libraries
149
+ - Node.js 26.7 or later
150
150
 
151
151
  The `@gtkx/native` addon ships prebuilt for x64 and arm64 glibc Linux; other targets need to build it from the GTKX repository, which requires a Rust toolchain.
152
152
 
@@ -167,7 +167,7 @@ GTKX is stable and ready for production use.
167
167
 
168
168
  ## Contributing
169
169
 
170
- Contributions are welcome. See [CONTRIBUTING.md](https://github.com/gtkx-org/gtkx/blob/main/CONTRIBUTING.md), the [Code of Conduct](https://github.com/gtkx-org/gtkx/blob/main/CODE_OF_CONDUCT.md), and the [security policy](https://github.com/gtkx-org/gtkx/blob/main/SECURITY.md). Building the repo needs Node.js 24 or later, pnpm, and a Rust toolchain.
170
+ Contributions are welcome. See [CONTRIBUTING.md](https://github.com/gtkx-org/gtkx/blob/main/CONTRIBUTING.md), the [Code of Conduct](https://github.com/gtkx-org/gtkx/blob/main/CODE_OF_CONDUCT.md), and the [security policy](https://github.com/gtkx-org/gtkx/blob/main/SECURITY.md). Building the repo needs Node.js 26.7 or later, pnpm, and a Rust toolchain.
171
171
 
172
172
  ## License
173
173
 
package/dist/command.d.ts CHANGED
@@ -8,6 +8,10 @@ declare const scaffoldCommand: import("citty").CommandDef<{
8
8
  readonly type: "string";
9
9
  readonly description: "Application ID (e.g., com.example.myapp)";
10
10
  };
11
+ readonly "display-name": {
12
+ readonly type: "string";
13
+ readonly description: "Application display name (e.g., My App)";
14
+ };
11
15
  readonly "package-manager": {
12
16
  readonly type: "string";
13
17
  readonly alias: "p";
@@ -34,7 +38,7 @@ declare const scaffoldCommand: import("citty").CommandDef<{
34
38
  readonly overwrite: {
35
39
  readonly type: "boolean";
36
40
  readonly alias: "f";
37
- readonly description: "Overwrite the contents of a non-empty target directory when running without prompts";
41
+ readonly description: "Replace scaffold files in a non-empty target directory when running without prompts";
38
42
  };
39
43
  }>;
40
44
  export { scaffoldCommand };
@@ -1 +1 @@
1
- {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAkBA,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+CnB,CAAC;AAgCH,OAAO,EAAE,eAAe,EAAE,CAAC"}
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAmBA,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmDnB,CAAC;AAiCH,OAAO,EAAE,eAAe,EAAE,CAAC"}
package/dist/command.js CHANGED
@@ -19,6 +19,10 @@ const scaffoldCommand = defineCommand({
19
19
  type: "string",
20
20
  description: "Application ID (e.g., com.example.myapp)",
21
21
  },
22
+ "display-name": {
23
+ type: "string",
24
+ description: "Application display name (e.g., My App)",
25
+ },
22
26
  "package-manager": {
23
27
  type: "string",
24
28
  alias: "p",
@@ -46,7 +50,7 @@ const scaffoldCommand = defineCommand({
46
50
  overwrite: {
47
51
  type: "boolean",
48
52
  alias: "f",
49
- description: "Overwrite the contents of a non-empty target directory when running without prompts",
53
+ description: "Replace scaffold files in a non-empty target directory when running without prompts",
50
54
  },
51
55
  },
52
56
  run: ({ args }) => runCreate(args),
@@ -66,6 +70,7 @@ const runCreate = async (args) => {
66
70
  await scaffold({
67
71
  name: args.name,
68
72
  applicationId: args["application-id"],
73
+ displayName: args["display-name"],
69
74
  packageManager: args["package-manager"],
70
75
  isTypescript: args.typescript,
71
76
  shouldIncludeTesting: args.vitest,
@@ -1 +1 @@
1
- {"version":3,"file":"command.js","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,gBAAgB,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,gCAAgC,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAa3C,MAAM,eAAe,GAAG,aAAa,CAAC;IAClC,IAAI,EAAE;QACF,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,+BAA+B;KAC/C;IACD,IAAI,EAAE;QACF,IAAI,EAAE;YACF,IAAI,EAAE,YAAY;YAClB,WAAW,EAAE,gEAAgE;YAC7E,QAAQ,EAAE,KAAK;SAClB;QACD,gBAAgB,EAAE;YACd,IAAI,EAAE,QAAQ;YACd,WAAW,EAAE,0CAA0C;SAC1D;QACD,iBAAiB,EAAE;YACf,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,GAAG;YACV,WAAW,EAAE,gCAAgC;SAChD;QACD,UAAU,EAAE;YACR,IAAI,EAAE,SAAS;YACf,mBAAmB,EAAE,8DAA8D;YACnF,WAAW,EAAE,wCAAwC;SACxD;QACD,MAAM,EAAE;YACJ,IAAI,EAAE,SAAS;YACf,WAAW,EAAE,gCAAgC;SAChD;QACD,GAAG,EAAE;YACD,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,GAAG;YACV,WAAW,EAAE,0DAA0D;SAC1E;QACD,gBAAgB,EAAE;YACd,IAAI,EAAE,SAAS;YACf,WAAW,EACP,yFAAyF;gBACzF,iBAAiB;SACxB;QACD,SAAS,EAAE;YACP,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,GAAG;YACV,WAAW,EAAE,qFAAqF;SACrG;KACJ;IACD,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC;CACrC,CAAC,CAAC;AAEH,MAAM,qBAAqB,GAAG,CAAC,KAAc,EAAQ,EAAE;IACnD,IAAI,KAAK,YAAY,sBAAsB,EAAE,CAAC;QAC1C,OAAO;IACX,CAAC;IAED,IAAI,CAAC,CAAC,KAAK,YAAY,oBAAoB,CAAC,EAAE,CAAC;QAC3C,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACzB,CAAC,CAAC;AAEF,MAAM,SAAS,GAAG,KAAK,EAAE,IAAuB,EAAiB,EAAE;IAC/D,MAAM,aAAa,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;IAEvF,IAAI,CAAC;QACD,MAAM,QAAQ,CAAC;YACX,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,aAAa,EAAE,IAAI,CAAC,gBAAgB,CAAC;YACrC,cAAc,EAAE,IAAI,CAAC,iBAAiB,CAAC;YACvC,YAAY,EAAE,IAAI,CAAC,UAAU;YAC7B,oBAAoB,EAAE,IAAI,CAAC,MAAM;YACjC,aAAa;YACb,eAAe,EAAE,IAAI,CAAC,SAAS;SAClC,CAAC,CAAC;IACP,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,qBAAqB,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;AACL,CAAC,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,CAAC","sourcesContent":["import * as p from \"@clack/prompts\";\nimport { errorMessage } from \"@gtkx/utils\";\nimport { defineCommand } from \"citty\";\nimport { OperationCanceledError, ScaffoldAbortedError } from \"./errors.js\";\nimport { PACKAGE_MANAGER_FLAG_DESCRIPTION } from \"./package-managers.js\";\nimport { scaffold } from \"./scaffolder.js\";\n\ntype CreateCommandArgs = {\n name?: string | undefined;\n \"application-id\"?: string | undefined;\n \"package-manager\"?: string | undefined;\n typescript?: boolean | undefined;\n vitest?: boolean | undefined;\n yes?: boolean | undefined;\n \"no-interactive\"?: boolean | undefined;\n overwrite?: boolean | undefined;\n};\n\nconst scaffoldCommand = defineCommand({\n meta: {\n name: \"create\",\n description: \"Create a new GTKX application\",\n },\n args: {\n name: {\n type: \"positional\",\n description: \"Target directory or project name (e.g. my-app, ., apps/my-app)\",\n required: false,\n },\n \"application-id\": {\n type: \"string\",\n description: \"Application ID (e.g., com.example.myapp)\",\n },\n \"package-manager\": {\n type: \"string\",\n alias: \"p\",\n description: PACKAGE_MANAGER_FLAG_DESCRIPTION,\n },\n typescript: {\n type: \"boolean\",\n negativeDescription: \"Scaffold the application in JavaScript instead of TypeScript\",\n description: \"Scaffold the application in TypeScript\",\n },\n vitest: {\n type: \"boolean\",\n description: \"Include a Vitest testing setup\",\n },\n yes: {\n type: \"boolean\",\n alias: \"y\",\n description: \"Skip prompts and accept defaults for unspecified options\",\n },\n \"no-interactive\": {\n type: \"boolean\",\n description:\n \"Run without prompts, using the default for every option not passed on the command line \" +\n \"(same as --yes)\",\n },\n overwrite: {\n type: \"boolean\",\n alias: \"f\",\n description: \"Overwrite the contents of a non-empty target directory when running without prompts\",\n },\n },\n run: ({ args }) => runCreate(args),\n});\n\nconst settleScaffoldFailure = (error: unknown): void => {\n if (error instanceof OperationCanceledError) {\n return;\n }\n\n if (!(error instanceof ScaffoldAbortedError)) {\n p.log.error(errorMessage(error));\n }\n\n process.exitCode = 1;\n};\n\nconst runCreate = async (args: CreateCommandArgs): Promise<void> => {\n const isInteractive = args[\"no-interactive\"] || args.yes ? false : process.stdin.isTTY;\n\n try {\n await scaffold({\n name: args.name,\n applicationId: args[\"application-id\"],\n packageManager: args[\"package-manager\"],\n isTypescript: args.typescript,\n shouldIncludeTesting: args.vitest,\n isInteractive,\n shouldOverwrite: args.overwrite,\n });\n } catch (error) {\n settleScaffoldFailure(error);\n }\n};\n\nexport { scaffoldCommand };\n"]}
1
+ {"version":3,"file":"command.js","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as p from \"@clack/prompts\";\nimport { errorMessage } from \"@gtkx/utils\";\nimport { defineCommand } from \"citty\";\nimport { OperationCanceledError, ScaffoldAbortedError } from \"./errors.js\";\nimport { PACKAGE_MANAGER_FLAG_DESCRIPTION } from \"./package-managers.js\";\nimport { scaffold } from \"./scaffolder.js\";\n\ntype CreateCommandArgs = {\n name?: string | undefined;\n \"application-id\"?: string | undefined;\n \"display-name\"?: string | undefined;\n \"package-manager\"?: string | undefined;\n typescript?: boolean | undefined;\n vitest?: boolean | undefined;\n yes?: boolean | undefined;\n \"no-interactive\"?: boolean | undefined;\n overwrite?: boolean | undefined;\n};\n\nconst scaffoldCommand = defineCommand({\n meta: {\n name: \"create\",\n description: \"Create a new GTKX application\",\n },\n args: {\n name: {\n type: \"positional\",\n description: \"Target directory or project name (e.g. my-app, ., apps/my-app)\",\n required: false,\n },\n \"application-id\": {\n type: \"string\",\n description: \"Application ID (e.g., com.example.myapp)\",\n },\n \"display-name\": {\n type: \"string\",\n description: \"Application display name (e.g., My App)\",\n },\n \"package-manager\": {\n type: \"string\",\n alias: \"p\",\n description: PACKAGE_MANAGER_FLAG_DESCRIPTION,\n },\n typescript: {\n type: \"boolean\",\n negativeDescription: \"Scaffold the application in JavaScript instead of TypeScript\",\n description: \"Scaffold the application in TypeScript\",\n },\n vitest: {\n type: \"boolean\",\n description: \"Include a Vitest testing setup\",\n },\n yes: {\n type: \"boolean\",\n alias: \"y\",\n description: \"Skip prompts and accept defaults for unspecified options\",\n },\n \"no-interactive\": {\n type: \"boolean\",\n description:\n \"Run without prompts, using the default for every option not passed on the command line \" +\n \"(same as --yes)\",\n },\n overwrite: {\n type: \"boolean\",\n alias: \"f\",\n description: \"Replace scaffold files in a non-empty target directory when running without prompts\",\n },\n },\n run: ({ args }) => runCreate(args),\n});\n\nconst settleScaffoldFailure = (error: unknown): void => {\n if (error instanceof OperationCanceledError) {\n return;\n }\n\n if (!(error instanceof ScaffoldAbortedError)) {\n p.log.error(errorMessage(error));\n }\n\n process.exitCode = 1;\n};\n\nconst runCreate = async (args: CreateCommandArgs): Promise<void> => {\n const isInteractive = args[\"no-interactive\"] || args.yes ? false : process.stdin.isTTY;\n\n try {\n await scaffold({\n name: args.name,\n applicationId: args[\"application-id\"],\n displayName: args[\"display-name\"],\n packageManager: args[\"package-manager\"],\n isTypescript: args.typescript,\n shouldIncludeTesting: args.vitest,\n isInteractive,\n shouldOverwrite: args.overwrite,\n });\n } catch (error) {\n settleScaffoldFailure(error);\n }\n};\n\nexport { scaffoldCommand };\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAKA,QAAA,MAAM,cAAc,GAAI,MAAM,MAAM,EAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,KAAG,IAO3F,CAAC;AAEF,OAAO,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAMA,QAAA,MAAM,cAAc,GAAI,MAAM,MAAM,EAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,KAAG,IAY3F,CAAC;AAEF,OAAO,EAAE,cAAc,EAAE,CAAC"}
package/dist/manifest.js CHANGED
@@ -1,13 +1,19 @@
1
- import { readFileSync, renameSync, writeFileSync } from "node:fs";
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  const PACKAGE_JSON_FILE = "package.json";
4
5
  const updateManifest = (root, mutate) => {
5
6
  const manifestPath = join(root, PACKAGE_JSON_FILE);
6
7
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
7
8
  mutate(manifest);
8
- const pendingPath = `${manifestPath}.pending`;
9
- writeFileSync(pendingPath, `${JSON.stringify(manifest, null, 4)}\n`);
10
- renameSync(pendingPath, manifestPath);
9
+ const pendingPath = join(root, `.package-${randomUUID()}.json`);
10
+ try {
11
+ writeFileSync(pendingPath, `${JSON.stringify(manifest, null, 4)}\n`, { flag: "wx", mode: 0o600 });
12
+ renameSync(pendingPath, manifestPath);
13
+ }
14
+ finally {
15
+ rmSync(pendingPath, { force: true });
16
+ }
11
17
  };
12
18
  export { updateManifest };
13
19
  //# sourceMappingURL=manifest.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,iBAAiB,GAAG,cAAc,CAAC;AAEzC,MAAM,cAAc,GAAG,CAAC,IAAY,EAAE,MAAmD,EAAQ,EAAE;IAC/F,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAA4B,CAAC;IAC3F,MAAM,CAAC,QAAQ,CAAC,CAAC;IACjB,MAAM,WAAW,GAAG,GAAG,YAAY,UAAU,CAAC;IAC9C,aAAa,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IACrE,UAAU,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;AAC1C,CAAC,CAAC;AAEF,OAAO,EAAE,cAAc,EAAE,CAAC","sourcesContent":["import { readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nconst PACKAGE_JSON_FILE = \"package.json\";\n\nconst updateManifest = (root: string, mutate: (manifest: Record<string, unknown>) => void): void => {\n const manifestPath = join(root, PACKAGE_JSON_FILE);\n const manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as Record<string, unknown>;\n mutate(manifest);\n const pendingPath = `${manifestPath}.pending`;\n writeFileSync(pendingPath, `${JSON.stringify(manifest, null, 4)}\\n`);\n renameSync(pendingPath, manifestPath);\n};\n\nexport { updateManifest };\n"]}
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,iBAAiB,GAAG,cAAc,CAAC;AAEzC,MAAM,cAAc,GAAG,CAAC,IAAY,EAAE,MAAmD,EAAQ,EAAE;IAC/F,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAA4B,CAAC;IAC3F,MAAM,CAAC,QAAQ,CAAC,CAAC;IACjB,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAE,YAAY,UAAU,EAAE,OAAO,CAAC,CAAC;IAEhE,IAAI,CAAC;QACD,aAAa,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAClG,UAAU,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;IAC1C,CAAC;YAAS,CAAC;QACP,MAAM,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,CAAC;AACL,CAAC,CAAC;AAEF,OAAO,EAAE,cAAc,EAAE,CAAC","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nconst PACKAGE_JSON_FILE = \"package.json\";\n\nconst updateManifest = (root: string, mutate: (manifest: Record<string, unknown>) => void): void => {\n const manifestPath = join(root, PACKAGE_JSON_FILE);\n const manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as Record<string, unknown>;\n mutate(manifest);\n const pendingPath = join(root, `.package-${randomUUID()}.json`);\n\n try {\n writeFileSync(pendingPath, `${JSON.stringify(manifest, null, 4)}\\n`, { flag: \"wx\", mode: 0o600 });\n renameSync(pendingPath, manifestPath);\n } finally {\n rmSync(pendingPath, { force: true });\n }\n};\n\nexport { updateManifest };\n"]}
@@ -1,6 +1,7 @@
1
1
  type CreateOptions = {
2
2
  name?: string | undefined;
3
3
  applicationId?: string | undefined;
4
+ displayName?: string | undefined;
4
5
  packageManager?: string | undefined;
5
6
  isTypescript?: boolean | undefined;
6
7
  shouldIncludeTesting?: boolean | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"scaffolder.d.ts","sourceRoot":"","sources":["../src/scaffolder.ts"],"names":[],"mappings":"AAsBA,KAAK,aAAa,GAAG;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,oBAAoB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACzC,CAAC;AAumBF,QAAA,MAAM,QAAQ,GAAU,UAAS,aAAkB,KAAG,OAAO,CAAC,IAAI,CAcjE,CAAC;AAEF,OAAO,EAAE,QAAQ,EAAE,CAAC"}
1
+ {"version":3,"file":"scaffolder.d.ts","sourceRoot":"","sources":["../src/scaffolder.ts"],"names":[],"mappings":"AAsBA,KAAK,aAAa,GAAG;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,oBAAoB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACzC,CAAC;AA0sBF,QAAA,MAAM,QAAQ,GAAU,UAAS,aAAkB,KAAG,OAAO,CAAC,IAAI,CAUjE,CAAC;AAEF,OAAO,EAAE,QAAQ,EAAE,CAAC"}
@@ -2,8 +2,8 @@ import * as p from "@clack/prompts";
2
2
  import { APPLICATION_ID_MAX_LENGTH, isValidApplicationId } from "@gtkx/config/internal";
3
3
  import { errorMessage, tryResolveExecutable, upperFirst } from "@gtkx/utils";
4
4
  import { execFileSync } from "node:child_process";
5
- import { mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
6
- import { basename, dirname, join, resolve } from "node:path";
5
+ import { lstatSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
6
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { addDependency, detectPackageManager as nypmDetectPackageManager } from "nypm";
8
8
  import { x } from "tinyexec";
9
9
  import packageManifest from "../package.json" with { type: "json" };
@@ -39,10 +39,7 @@ const APPLICATION_ID_PREFIX = "com.";
39
39
  const APPLICATION_ID_SUFFIX = ".app";
40
40
  const APPLICATION_ID_SEGMENT_LIMIT = APPLICATION_ID_MAX_LENGTH - APPLICATION_ID_PREFIX.length -
41
41
  APPLICATION_ID_SUFFIX.length;
42
- const pinGtkxDependency = (name, version) => name.startsWith("@gtkx/") ? `${name}@^${version}` : name;
43
- const getDevCommand = (packageManager) => DEV_COMMAND[packageManager];
44
- const getAddCommand = (packageManager) => ADD_COMMAND[packageManager];
45
- const titleFromName = (name) => name.split("-").map((part) => upperFirst(part)).join(" ");
42
+ const displayNameFromProjectName = (name) => name.split("-").map((part) => upperFirst(part)).join(" ");
46
43
  const gitConfigValue = (key) => {
47
44
  const git = tryResolveExecutable("git");
48
45
  if (git === undefined) {
@@ -74,6 +71,10 @@ const isDirEmpty = (dir) => {
74
71
  const entries = readdirSync(dir);
75
72
  return entries.length === 0 || (entries.length === 1 && entries[0] === ".git");
76
73
  };
74
+ const shouldInitializeGit = (root) => {
75
+ const entry = lstatSync(root, { throwIfNoEntry: false });
76
+ return entry === undefined || (entry.isDirectory() && readdirSync(root).length === 0);
77
+ };
77
78
  const getDevDependencies = ({ isTypescript, shouldIncludeTesting, }) => {
78
79
  const devDeps = [...DEV_DEPENDENCIES];
79
80
  if (isTypescript) {
@@ -103,6 +104,13 @@ const validateProjectName = (value) => {
103
104
  const validateApplicationIdFormat = (value) => isValidApplicationId(value) ? undefined : APPLICATION_ID_FORMAT_ERROR;
104
105
  const validateApplicationIdInput = (value) => value ? validateApplicationIdFormat(value) : "Application ID is required";
105
106
  const validateApplicationIdAnswer = (value) => value ? validateApplicationIdFormat(value) : undefined;
107
+ const validateDisplayName = (value) => {
108
+ if (value === undefined || value.trim().length === 0) {
109
+ return "Display name is required";
110
+ }
111
+ return value.includes("\n") || value.includes("\r") ? "Display name must be a single line" : undefined;
112
+ };
113
+ const validateDisplayNameAnswer = (value) => value ? validateDisplayName(value) : undefined;
106
114
  const fail = (message) => {
107
115
  p.log.error(message);
108
116
  throw new ScaffoldAbortedError(message);
@@ -116,14 +124,6 @@ const requestedPackageManager = (value) => {
116
124
  }
117
125
  return value;
118
126
  };
119
- const targetStats = (root) => {
120
- try {
121
- return statSync(root, { throwIfNoEntry: false });
122
- }
123
- catch {
124
- return undefined;
125
- }
126
- };
127
127
  const validateTargetDir = (target) => {
128
128
  if (!target) {
129
129
  return "Project directory is required";
@@ -145,29 +145,29 @@ const promptApplicationId = async (name) => {
145
145
  validate: validateApplicationIdAnswer,
146
146
  }));
147
147
  };
148
- const detectPackageManager = async (cwd) => {
149
- const detected = await nypmDetectPackageManager(cwd, { includeParentDirs: true });
150
- if (!detected) {
151
- return undefined;
152
- }
153
- return isKnownPackageManager(detected.name) ? detected.name : undefined;
148
+ const promptDisplayName = async (name) => {
149
+ const defaultDisplayName = displayNameFromProjectName(name);
150
+ return guardCancellation(await p.text({
151
+ message: "Display name",
152
+ placeholder: defaultDisplayName,
153
+ defaultValue: defaultDisplayName,
154
+ validate: validateDisplayNameAnswer,
155
+ }));
154
156
  };
155
- const packageManagerHint = (manager, detected) => {
157
+ const packageManagerOption = (manager, detected) => {
158
+ let hint;
156
159
  if (detected === manager.value) {
157
- return "detected";
160
+ hint = "detected";
158
161
  }
159
- if (manager.isRecommended) {
160
- return "recommended";
162
+ else if (manager.isRecommended) {
163
+ hint = "recommended";
161
164
  }
162
- return undefined;
163
- };
164
- const packageManagerOption = (manager, detected) => {
165
- const hint = packageManagerHint(manager, detected);
166
165
  return { value: manager.value, label: manager.label, ...(hint !== undefined && { hint }) };
167
166
  };
168
167
  const detectedPackageManager = async () => {
169
168
  try {
170
- return await detectPackageManager(process.cwd());
169
+ const detected = await nypmDetectPackageManager(process.cwd(), { includeParentDirs: true });
170
+ return detected && isKnownPackageManager(detected.name) ? detected.name : undefined;
171
171
  }
172
172
  catch {
173
173
  return undefined;
@@ -182,52 +182,34 @@ const promptPackageManager = async () => {
182
182
  initialValue: initial,
183
183
  }));
184
184
  };
185
- const shouldUseTypeScript = async () => guardCancellation(await p.confirm({
186
- message: "Use TypeScript?",
187
- initialValue: true,
188
- }));
189
- const shouldSetUpTesting = async () => guardCancellation(await p.confirm({
190
- message: "Include testing setup (Vitest)?",
191
- initialValue: true,
192
- }));
193
- const emptyDir = (root) => {
194
- for (const entry of readdirSync(root)) {
195
- if (entry === ".git") {
196
- continue;
197
- }
198
- rmSync(join(root, entry), { recursive: true, force: true });
199
- }
200
- };
201
- const prepareTargetDirectory = (root, shouldEmptyTarget) => {
202
- mkdirSync(root, { recursive: true });
203
- if (shouldEmptyTarget) {
204
- emptyDir(root);
205
- }
206
- };
185
+ const isOptionEnabled = async (value, isInteractive, message) => value ??
186
+ (isInteractive
187
+ ? guardCancellation(await p.confirm({ message, initialValue: true }))
188
+ : true);
207
189
  const shouldOverwriteDirectory = async (target, options) => {
208
190
  if (!options.isInteractive) {
209
191
  return options.shouldOverwrite === true;
210
192
  }
211
193
  return guardCancellation(await p.confirm({
212
- message: `Directory "${target}" is not empty. Overwrite its contents?`,
194
+ message: `Directory "${target}" is not empty. Replace scaffold files?`,
213
195
  initialValue: false,
214
196
  }));
215
197
  };
216
- const shouldEmptyTargetDirectory = async (root, target, options) => {
217
- const stats = targetStats(root);
198
+ const validateTargetAvailability = async (root, target, options) => {
199
+ const stats = statSync(root, { throwIfNoEntry: false });
218
200
  if (stats === undefined) {
219
- return false;
201
+ return;
220
202
  }
221
203
  if (!stats.isDirectory()) {
222
204
  return fail(`Target "${target}" is not a directory`);
223
205
  }
224
206
  if (isDirEmpty(root)) {
225
- return false;
207
+ return;
226
208
  }
227
209
  if (await shouldOverwriteDirectory(target, options)) {
228
- return true;
210
+ return;
229
211
  }
230
- return fail(`Directory "${target}" is not empty`);
212
+ fail(`Directory "${target}" is not empty`);
231
213
  };
232
214
  const resolveTarget = async (options) => {
233
215
  if (options.name === undefined && options.isInteractive) {
@@ -243,6 +225,12 @@ const resolveApplicationId = async (options, name) => {
243
225
  const error = validateApplicationIdInput(applicationId);
244
226
  return error === undefined ? applicationId : fail(error);
245
227
  };
228
+ const resolveDisplayName = async (options, name) => {
229
+ const value = options.displayName ??
230
+ (options.isInteractive ? await promptDisplayName(name) : displayNameFromProjectName(name));
231
+ const error = validateDisplayName(value);
232
+ return error === undefined ? value.trim() : fail(error);
233
+ };
246
234
  const resolvePackageManager = async (requested, options) => {
247
235
  if (requested !== undefined) {
248
236
  return requested;
@@ -252,39 +240,30 @@ const resolvePackageManager = async (requested, options) => {
252
240
  }
253
241
  return (await detectedPackageManager()) ?? "pnpm";
254
242
  };
255
- const isTypescriptSelected = async (options) => {
256
- if (options.isTypescript !== undefined) {
257
- return options.isTypescript;
258
- }
259
- return options.isInteractive ? shouldUseTypeScript() : true;
260
- };
261
- const isTestingIncluded = async (options) => {
262
- if (options.shouldIncludeTesting !== undefined) {
263
- return options.shouldIncludeTesting;
264
- }
265
- return options.isInteractive ? shouldSetUpTesting() : true;
266
- };
267
243
  const resolveOptions = async (options) => {
268
244
  const requested = requestedPackageManager(options.packageManager);
269
245
  const target = await resolveTarget(options);
270
246
  const name = deriveProjectName(target);
247
+ const displayName = await resolveDisplayName(options, name);
271
248
  const applicationId = await resolveApplicationId(options, name);
272
249
  const root = resolve(process.cwd(), target);
273
250
  const isCurrentDirectory = root === process.cwd();
274
- const shouldEmptyTarget = await shouldEmptyTargetDirectory(root, target, options);
251
+ const shouldInitialize = shouldInitializeGit(root);
252
+ await validateTargetAvailability(root, target, options);
275
253
  const packageManager = await resolvePackageManager(requested, options);
276
- const isTypescript = await isTypescriptSelected(options);
277
- const shouldIncludeTesting = await isTestingIncluded(options);
254
+ const isTypescript = await isOptionEnabled(options.isTypescript, options.isInteractive, "Use TypeScript?");
255
+ const shouldIncludeTesting = await isOptionEnabled(options.shouldIncludeTesting, options.isInteractive, "Include testing setup (Vitest)?");
278
256
  return {
279
257
  target,
280
258
  root,
281
259
  isCurrentDirectory,
282
260
  name,
261
+ displayName,
283
262
  applicationId,
284
263
  packageManager,
285
264
  isTypescript,
286
265
  shouldIncludeTesting,
287
- shouldEmptyTarget,
266
+ shouldInitializeGit: shouldInitialize,
288
267
  };
289
268
  };
290
269
  const isTemplateIncluded = (templateRelativePath, resolved) => {
@@ -296,37 +275,112 @@ const isTemplateIncluded = (templateRelativePath, resolved) => {
296
275
  }
297
276
  return true;
298
277
  };
299
- const toScriptExtension = (templateRelativePath, isTypescript) => isTypescript ? templateRelativePath : templateRelativePath.replace(/\.tsx$/, ".jsx").replace(/\.ts$/, ".js");
300
- const toIconPath = (path, applicationId) => path.endsWith(ICON_TEMPLATE_NAME) ? `${path.slice(0, -ICON_TEMPLATE_NAME.length)}${applicationId}.svg` : path;
301
- const toDestinationPath = (templateRelativePath, isTypescript, applicationId) => toIconPath(toScriptExtension(templateRelativePath, isTypescript), applicationId);
302
- const addTestScript = (root) => {
303
- updateManifest(root, (manifest) => {
304
- manifest.scripts.test = "vitest run";
305
- });
278
+ const destinationPath = (template, resolved) => {
279
+ const scriptPath = resolved.isTypescript ? template : template.replace(/\.tsx$/, ".jsx").replace(/\.ts$/, ".js");
280
+ return scriptPath.endsWith(ICON_TEMPLATE_NAME)
281
+ ? `${scriptPath.slice(0, -ICON_TEMPLATE_NAME.length)}${resolved.applicationId}.svg`
282
+ : scriptPath;
306
283
  };
307
284
  const templateContext = (resolved) => ({
308
285
  name: resolved.name,
309
286
  applicationId: resolved.applicationId,
310
- title: titleFromName(resolved.name),
287
+ displayName: resolved.displayName,
311
288
  shouldIncludeTesting: resolved.shouldIncludeTesting,
312
289
  isTypescript: resolved.isTypescript,
313
290
  importExtension: resolved.isTypescript ? ".js" : ".jsx",
314
- developerName: gitConfigValue("user.name") ?? titleFromName(resolved.name),
291
+ developerName: gitConfigValue("user.name") ?? resolved.displayName,
315
292
  developerEmail: gitConfigValue("user.email"),
316
293
  });
317
- const scaffoldProject = async (root, resolved) => {
318
- const { applicationId, isTypescript, shouldIncludeTesting } = resolved;
294
+ const replaceTemplateDestination = (destination) => {
295
+ const entry = lstatSync(destination, { throwIfNoEntry: false });
296
+ if (entry === undefined) {
297
+ return;
298
+ }
299
+ if (!entry.isFile() && !entry.isSymbolicLink()) {
300
+ fail(`Cannot replace scaffold file because its destination is not a file: ${destination}`);
301
+ }
302
+ rmSync(destination, { force: true });
303
+ };
304
+ const projectRelativeDestination = (root, destination) => {
305
+ const relativePath = relative(root, destination);
306
+ const isOutsideProject = relativePath.length === 0 || relativePath.startsWith(`..${sep}`) ||
307
+ isAbsolute(relativePath);
308
+ if (isOutsideProject) {
309
+ fail(`Scaffold destination is outside the project: ${destination}`);
310
+ }
311
+ return relativePath;
312
+ };
313
+ const assertRootIsNotSymbolicLink = (root) => {
314
+ const rootEntry = lstatSync(root, { throwIfNoEntry: false });
315
+ if (rootEntry?.isSymbolicLink()) {
316
+ fail(`Cannot scaffold through a symbolic link: ${root}`);
317
+ }
318
+ };
319
+ const assertSafeAncestor = (ancestor) => {
320
+ const entry = lstatSync(ancestor, { throwIfNoEntry: false });
321
+ if (entry?.isSymbolicLink()) {
322
+ fail(`Cannot scaffold through a symbolic link: ${ancestor}`);
323
+ }
324
+ if (entry !== undefined && !entry.isDirectory()) {
325
+ fail(`Cannot scaffold through a non-directory path: ${ancestor}`);
326
+ }
327
+ };
328
+ const assertSafeAncestors = (root, relativePath) => {
329
+ let ancestor = root;
330
+ const segments = relativePath.split(sep);
331
+ const ancestorSegments = segments.slice(0, -1);
332
+ for (const segment of ancestorSegments) {
333
+ ancestor = join(ancestor, segment);
334
+ assertSafeAncestor(ancestor);
335
+ }
336
+ };
337
+ const assertReplaceableDestination = (destination) => {
338
+ const entry = lstatSync(destination, { throwIfNoEntry: false });
339
+ const isReplaceable = entry === undefined || entry.isFile() || entry.isSymbolicLink();
340
+ if (!isReplaceable) {
341
+ fail(`Cannot replace scaffold file because its destination is not a file: ${destination}`);
342
+ }
343
+ };
344
+ const assertSafeDestination = (root, destination) => {
345
+ const relativePath = projectRelativeDestination(root, destination);
346
+ assertRootIsNotSymbolicLink(root);
347
+ assertSafeAncestors(root, relativePath);
348
+ assertReplaceableDestination(destination);
349
+ };
350
+ const plannedScaffoldFiles = async (root, resolved) => {
319
351
  const context = templateContext(resolved);
352
+ const files = [];
320
353
  for (const template of listTemplates()) {
321
- if (!isTemplateIncluded(template, resolved)) {
322
- continue;
354
+ if (isTemplateIncluded(template, resolved)) {
355
+ files.push({
356
+ destination: join(root, destinationPath(template, resolved)),
357
+ contents: await renderFile(template, context),
358
+ });
323
359
  }
324
- const destination = join(root, toDestinationPath(template, isTypescript, applicationId));
325
- mkdirSync(dirname(destination), { recursive: true });
326
- writeFileSync(destination, await renderFile(template, context));
327
360
  }
328
- if (shouldIncludeTesting) {
329
- addTestScript(root);
361
+ return files;
362
+ };
363
+ const auxiliaryDestinations = (root, resolved) => [
364
+ ...(resolved.packageManager === "pnpm" ? [join(root, "pnpm-workspace.yaml")] : []),
365
+ ...(resolved.isTypescript ? [join(root, "node_modules", ".gtkx", "env.d.ts")] : []),
366
+ ];
367
+ const scaffoldProject = async (root, resolved) => {
368
+ const files = await plannedScaffoldFiles(root, resolved);
369
+ const destinations = [...files.map((file) => file.destination), ...auxiliaryDestinations(root, resolved)];
370
+ for (const destination of destinations) {
371
+ assertSafeDestination(root, destination);
372
+ }
373
+ for (const destination of destinations) {
374
+ replaceTemplateDestination(destination);
375
+ }
376
+ for (const file of files) {
377
+ mkdirSync(dirname(file.destination), { recursive: true });
378
+ writeFileSync(file.destination, file.contents);
379
+ }
380
+ if (resolved.shouldIncludeTesting) {
381
+ updateManifest(root, (manifest) => {
382
+ manifest.scripts.test = "vitest run";
383
+ });
330
384
  }
331
385
  };
332
386
  const withoutInheritedScriptPolicy = async (run) => {
@@ -351,9 +405,9 @@ const installDependencies = async (options) => {
351
405
  await addDependency(dependencies, { cwd, packageManager, dev: isDev, silent: true });
352
406
  });
353
407
  };
354
- const pin = (names) => names.map((dependency) => pinGtkxDependency(dependency, selfVersion));
408
+ const pin = (names) => names.map((dependency) => dependency.startsWith("@gtkx/") ? `${dependency}@^${selfVersion}` : dependency);
355
409
  const formatRecovery = ({ resolved, devDependencies }) => {
356
- const add = getAddCommand(resolved.packageManager);
410
+ const add = ADD_COMMAND[resolved.packageManager];
357
411
  const steps = [
358
412
  ...(resolved.isCurrentDirectory ? [] : [`cd ${resolved.target}`]),
359
413
  `${add} ${pin(DEPENDENCIES).join(" ")}`,
@@ -404,7 +458,23 @@ const writeInitialEnvModule = (root) => {
404
458
  mkdirSync(storeDir, { recursive: true });
405
459
  writeFileSync(join(storeDir, "env.d.ts"), `${GTKX_ENV_MODULE_HEADER}\n`);
406
460
  };
407
- const initializeGitRepo = async (root) => {
461
+ const isInsideGitRepository = (root) => {
462
+ const git = tryResolveExecutable("git");
463
+ if (git === undefined) {
464
+ return false;
465
+ }
466
+ try {
467
+ execFileSync(git, ["-C", root, "rev-parse", "--show-toplevel"], { stdio: "ignore" });
468
+ return true;
469
+ }
470
+ catch {
471
+ return false;
472
+ }
473
+ };
474
+ const initializeGitRepo = async (root, shouldInitialize) => {
475
+ if (!shouldInitialize || isInsideGitRepository(root)) {
476
+ return;
477
+ }
408
478
  const spinner = p.spinner();
409
479
  spinner.start("Initializing git repository...");
410
480
  try {
@@ -419,7 +489,7 @@ const initializeGitRepo = async (root) => {
419
489
  }
420
490
  };
421
491
  const printNextSteps = (resolved) => {
422
- const devCmd = getDevCommand(resolved.packageManager);
492
+ const devCmd = DEV_COMMAND[resolved.packageManager];
423
493
  const cdStep = resolved.isCurrentDirectory ? "" : `cd ${resolved.target}\n`;
424
494
  const testingNote = resolved.shouldIncludeTesting ? HEADLESS_COMPOSITOR_NOTE : "";
425
495
  p.note(`${cdStep}${devCmd}${testingNote}`, "Next steps");
@@ -430,9 +500,12 @@ const createProjectStructure = async (root, resolved) => {
430
500
  done: "Project structure created",
431
501
  failed: "Failed to create the project structure",
432
502
  run: async () => {
433
- prepareTargetDirectory(root, resolved.shouldEmptyTarget);
503
+ mkdirSync(root, { recursive: true });
434
504
  await scaffoldProject(root, resolved);
435
505
  writeBuildAllowance(root, resolved.packageManager);
506
+ if (resolved.isTypescript) {
507
+ writeInitialEnvModule(root);
508
+ }
436
509
  },
437
510
  });
438
511
  };
@@ -443,10 +516,7 @@ const scaffold = async (options = {}) => {
443
516
  const devDeps = getDevDependencies(resolved);
444
517
  await createProjectStructure(root, resolved);
445
518
  await installAllDependencies({ resolved, devDependencies: devDeps });
446
- if (resolved.isTypescript) {
447
- writeInitialEnvModule(root);
448
- }
449
- await initializeGitRepo(root);
519
+ await initializeGitRepo(root, resolved.shouldInitializeGit);
450
520
  printNextSteps(resolved);
451
521
  };
452
522
  export { scaffold };