@lingfan-engine/create 0.3.3 → 0.3.5

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 (34) hide show
  1. package/bin/create.mjs +7 -3
  2. package/package.json +1 -1
  3. package/src/cli/answers.ts +275 -0
  4. package/src/cli/cli.ts +93 -194
  5. package/src/cli/index.ts +29 -0
  6. package/src/cli/install.ts +60 -0
  7. package/src/cli/prompts.ts +230 -31
  8. package/src/cli/stage.ts +113 -0
  9. package/src/generate/index.ts +7 -2
  10. package/src/generate/project.ts +99 -34
  11. package/src/overlay/index.ts +1 -0
  12. package/src/overlay/merge.ts +27 -1
  13. package/src/registry/index.ts +4 -0
  14. package/src/registry/template-meta.ts +73 -0
  15. package/src/shared/guards.ts +14 -0
  16. package/src/shared/index.ts +10 -1
  17. package/src/shared/package-manager.ts +87 -0
  18. package/src/shared/texts.ts +45 -17
  19. package/template/README.md +59 -32
  20. package/template/__PROJECT__/gitignore +2 -0
  21. package/template/__PROJECT__/package.json +2 -1
  22. package/template/__PROJECT__/package.patch.json +5 -0
  23. package/template/__PROJECT__/src/host/index.ts +1 -0
  24. package/template/__PROJECT__/src/host/native-ports.ts +56 -0
  25. package/template/__PROJECT__/src/main.ts +5 -2
  26. package/template/overlays/README.md +6 -0
  27. package/template/overlays/react/src/main.tsx +3 -0
  28. package/template/overlays/story/Resources/Stories/{title/title_main.story → manual/manual_intro.story} +15 -10
  29. package/template/overlays/story/package.patch.json +1 -17
  30. package/template/overlays/tauri/README.md +6 -5
  31. package/template/overlays/tauri/package.patch.json +1 -4
  32. package/template/overlays/vue/src/main.ts +3 -0
  33. package/template/template-meta.json +16 -12
  34. package/template/overlays/tauri/src/main.ts +0 -101
@@ -0,0 +1,29 @@
1
+ /**
2
+ * CLI 域唯一出口。
3
+ *
4
+ * 只做转发:参数解析(`cli.ts`)、答案合成(`answers.ts`)、交互问答(`prompts.ts`)、
5
+ * 生成编排(`stage.ts`)、依赖安装与后续命令渲染(`install.ts`)各自成文件,
6
+ * 调用方从这里进出,不穿过文件名。
7
+ */
8
+ export { parseArgs, runCreateCli } from "./cli.ts";
9
+ export type { CreateArgs } from "./cli.ts";
10
+ export { resolveAnswers } from "./answers.ts";
11
+ export type { AnswerSources, AnswerTexts, CreateAnswers } from "./answers.ts";
12
+ export {
13
+ askDescription,
14
+ askExisting,
15
+ askLayers,
16
+ askProjectName,
17
+ createTerminalIo,
18
+ findLayerConflicts,
19
+ parseLayerAnswer,
20
+ pickOption,
21
+ } from "./prompts.ts";
22
+ export type { PromptIo, PromptTexts } from "./prompts.ts";
23
+ export { runStage } from "./stage.ts";
24
+ export type { StageOptions } from "./stage.ts";
25
+ export {
26
+ fillManagerCommands,
27
+ installProject,
28
+ renderNextSteps,
29
+ } from "./install.ts";
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 生成之后的两个副作用:装依赖、打印下一步。
3
+ *
4
+ * 为什么单独成文件:这两件事都要按调用方的包管理器变写法,而组合编排
5
+ * (`cli.ts`)不该关心命令怎么拼。生成器是给**别人**用的工具,本仓开发固定
6
+ * pnpm 是仓库约定,不是产物约定——`npm create` 起的进程就该看到 npm 的命令。
7
+ */
8
+ import { spawn } from "node:child_process";
9
+ import type { CreateTexts, PackageManager } from "../shared/index.ts";
10
+
11
+ /**
12
+ * 在生成的工程里装依赖。
13
+ *
14
+ * 命令与可执行名都取自 `manager`:装了 npm 的人未必装了 pnpm,反过来也一样,
15
+ * 猜错就是一句「找不到命令」。`shell` 在 Windows 上是必需的——npm 与 pnpm 的
16
+ * 可执行是 `.cmd`,不带 shell 直接 spawn 会 ENOENT。
17
+ */
18
+ export async function installProject(
19
+ dir: string,
20
+ manager: PackageManager,
21
+ ): Promise<boolean> {
22
+ return await new Promise<boolean>((done) => {
23
+ const child = spawn(manager.id, [manager.install], {
24
+ cwd: dir,
25
+ stdio: "inherit",
26
+ shell: process.platform === "win32",
27
+ });
28
+ child.on("error", () => done(false));
29
+ child.on("close", (code) => done(code === 0));
30
+ });
31
+ }
32
+
33
+ /**
34
+ * 把文案里的 `<install>` / `<dev>` 换成该包管理器的实际写法。
35
+ *
36
+ * 生成后的提示若写死 `pnpm install`,用 npm 的人第一步就会撞上「没有这个命令」
37
+ * ——这类提示写错比不写更糟,因为它看起来像官方指引。
38
+ */
39
+ export function fillManagerCommands(
40
+ text: string,
41
+ manager: PackageManager,
42
+ ): string {
43
+ return text
44
+ .split("<install>")
45
+ .join(`${manager.id} ${manager.install}`)
46
+ .split("<dev>")
47
+ .join(`${manager.id} ${manager.script}`);
48
+ }
49
+
50
+ /** 生成后的三步提示:`cd` 到工程目录、装依赖、跑起来 */
51
+ export function renderNextSteps(
52
+ projectDir: string,
53
+ manager: PackageManager,
54
+ texts: CreateTexts,
55
+ ): string {
56
+ return fillManagerCommands(
57
+ texts.nextSteps.split("<name>").join(projectDir),
58
+ manager,
59
+ );
60
+ }
@@ -1,28 +1,49 @@
1
1
  /**
2
2
  * 交互问答。
3
3
  *
4
- * 生成器要问四件事:工程名、框架、壳、故事源形态。四项都能用命令行参数预先给定——CI、
5
- * 脚本、或「我已经知道要什么」的人不该被问答拦住,因此问答只在参数缺席时发生。
4
+ * 生成器按固定顺序问三件事:工程名 → 描述 → 形态。顺序不能换:工程名决定目录与
5
+ * 包名,描述与形态都是在「这个工程」之上的补充;先问后者再问名字,人得回头改口。
6
+ *
7
+ * 形态是**一次多选**而不是三个单选:可叠加的层是一张网,不是三根互不相干的轴——
8
+ * 选 Vue 就排除了小程序,选桌面壳又与小程序互斥。逐个维度问会把矛盾留到最后才
9
+ * 暴露,一次问清则当场说穿重问。
10
+ *
11
+ * 目标目录非空时另问一次怎么处置:那是覆盖别人的东西,必须由使用者当场表态,
12
+ * 不能拿缺省值糊过去(缺省是取消)。
6
13
  *
7
14
  * 实现只用 `node:readline` 的 promise 版:它随 Node 自带,不引第三方交互库。
8
- * 提问写 stderr 而不是 stdout,产物路径之类的结果才走 stdout,这样
9
- * `npm create … > log.txt` 不会把问题一起吞掉。
15
+ * 提问写 stderr 而不是 stdout,这样 `npm create … > log.txt` 不会把问题一起吞掉。
10
16
  */
11
17
  import { createInterface } from "node:readline/promises";
12
- import { isValidProjectName, suggestProjectName } from "../shared/index.ts";
13
- import type { TemplateOption } from "../registry/index.ts";
18
+ import {
19
+ isValidProjectName,
20
+ normalizeDescription,
21
+ suggestProjectName,
22
+ } from "../shared/index.ts";
23
+ import type { LayerConflict, TemplateOption } from "../registry/index.ts";
24
+ import type { ExistingTarget } from "../generate/index.ts";
14
25
 
15
26
  /** 问答需要的一份文案;由调用方从共享文案表传入,便于测试替换 */
16
27
  export interface PromptTexts {
17
28
  readonly promptProjectName: string;
18
- readonly promptFramework: string;
19
- readonly promptShell: string;
20
- readonly promptSources: string;
29
+ readonly promptDescription: string;
30
+ readonly descriptionHint: string;
31
+ readonly promptLayers: string;
32
+ readonly nonInteractive: string;
33
+ readonly nonInteractiveHint: string;
34
+ readonly promptExisting: string;
21
35
  readonly nameRequired: string;
22
36
  readonly nameInvalid: string;
23
- readonly frameworkUnknown: string;
24
- readonly shellUnknown: string;
25
- readonly sourcesUnknown: string;
37
+ readonly layersEmptyHint: string;
38
+ readonly layersInvalid: string;
39
+ readonly layersConflict: string;
40
+ readonly layersConflictHint: string;
41
+ readonly existingIgnore: string;
42
+ readonly existingIgnoreHint: string;
43
+ readonly existingClear: string;
44
+ readonly existingClearHint: string;
45
+ readonly existingCancel: string;
46
+ readonly existingCancelHint: string;
26
47
  }
27
48
 
28
49
  /** 问答依赖的输入输出面;默认接真实终端,测试可换成脚本化实现 */
@@ -31,15 +52,48 @@ export interface PromptIo {
31
52
  close(): void;
32
53
  }
33
54
 
34
- /** 造一个接到标准输入输出的问答面 */
35
- export function createTerminalIo(): PromptIo {
55
+ /** 输入已关闭时的失败信息:调用方会把它印出来并退出,因此要人读得懂 */
56
+ const INPUT_CLOSED = "标准输入已关闭,无法继续问答";
57
+
58
+ /**
59
+ * 造一个接到标准输入输出的问答面。
60
+ *
61
+ * 参数是为了可测:非交互终端下 `question()` 必须失败而不是永远挂着。`readline` 在
62
+ * 输入流结束后只会发一个 `close` 事件,它自己的 promise 因此既不 resolve 也不
63
+ * reject——进程于是停在「等一个永远不会到的回车」,靠外部超时才退出。这里把
64
+ * `close` 转成 reject,让失败变成一次可捕获的异常。
65
+ */
66
+ export function createTerminalIo(
67
+ input: NodeJS.ReadableStream = process.stdin,
68
+ output: NodeJS.WritableStream = process.stderr,
69
+ ): PromptIo {
36
70
  const rl = createInterface({
37
- input: process.stdin,
38
- output: process.stderr,
39
- terminal: process.stdin.isTTY === true,
71
+ input,
72
+ output,
73
+ terminal: input === process.stdin && process.stdin.isTTY === true,
74
+ });
75
+ let closed = false;
76
+ rl.once("close", () => {
77
+ closed = true;
40
78
  });
41
79
  return {
42
- question: (text) => rl.question(text),
80
+ question: (text) => {
81
+ if (closed) return Promise.reject(new Error(INPUT_CLOSED));
82
+ return new Promise<string>((resolve, reject) => {
83
+ const fail = (): void => reject(new Error(INPUT_CLOSED));
84
+ rl.once("close", fail);
85
+ rl.question(text).then(
86
+ (answer) => {
87
+ rl.off("close", fail);
88
+ resolve(answer);
89
+ },
90
+ (error: unknown) => {
91
+ rl.off("close", fail);
92
+ reject(error instanceof Error ? error : new Error(String(error)));
93
+ },
94
+ );
95
+ });
96
+ },
43
97
  close: () => rl.close(),
44
98
  };
45
99
  }
@@ -67,30 +121,107 @@ export async function askProjectName(
67
121
  }
68
122
 
69
123
  /**
70
- * 从选项表里选一项:输入序号(从 1 起)或直接输入 id,空输入取首项。
124
+ * 问描述:只问一次,回车即空。
71
125
  *
72
- * 两种取值都认,是因为脚本里写 `--framework vue` 更自然,而交互时按数字更省事;
73
- * 让同一套问答支持两种输入,参数模式与交互模式就不必分两套校验。
126
+ * 不重问也不校验:描述只影响清单里的一行说明,不影响任何路径与命令。为难一个
127
+ * 可以留空的输入框,只会让人多点几次回车。超长按上限截断——`package.json` 的
128
+ * 描述在工具链里本就是短文本,与其被别处悄悄截,不如自己截。
74
129
  */
75
- export async function askOption(
130
+ export async function askDescription(
76
131
  io: PromptIo,
77
- label: string,
78
- options: readonly TemplateOption[],
79
- fallbackId: string | undefined,
80
- invalidText: string,
132
+ texts: PromptTexts,
81
133
  ): Promise<string> {
82
- if (fallbackId !== undefined) return fallbackId;
134
+ const answer = await io.question(
135
+ `${texts.promptDescription}(${texts.descriptionHint}):`,
136
+ );
137
+ return normalizeDescription(answer);
138
+ }
139
+
140
+ /**
141
+ * 问形态:一次多选,空格或逗号分隔,回车即空集。
142
+ *
143
+ * 选项表由调用方按「这一层有没有目录」筛过,所以这里看不到 `none`/`browser`/`ts`
144
+ * 这类缺省层——它们不是一个可以叠加的东西,而是「什么都不叠」。
145
+ *
146
+ * 互斥对由元数据的 `excludes` 推导,本文件不认识任何具体层 id:新增一层只要在
147
+ * 元数据里声明互斥,问答自动跟着变。撞上互斥就说穿是哪两层、然后重问同一问,
148
+ * 不静默丢掉其中一层——丢掉会让「我明明选了小程序」无法解释。
149
+ */
150
+ export async function askLayers(
151
+ io: PromptIo,
152
+ texts: PromptTexts,
153
+ layers: readonly TemplateOption[],
154
+ catalogue: readonly TemplateOption[],
155
+ ): Promise<string[]> {
156
+ const labels = new Map(catalogue.map((option) => [option.id, option.label]));
157
+ const lines = layers
158
+ .map((option, index) => {
159
+ const excludes = (option.excludes ?? []).map(
160
+ (id) => labels.get(id) ?? id,
161
+ );
162
+ const tail =
163
+ excludes.length > 0 ? ` 互斥:${excludes.join("、")}` : " 互斥:无";
164
+ return ` ${index + 1}. ${option.label} —— ${option.hint}${tail}`;
165
+ })
166
+ .join("\n");
167
+ process.stderr.write(`${texts.promptLayers}:\n${lines}\n`);
168
+ for (;;) {
169
+ const answer = (
170
+ await io.question(
171
+ `选择(1-${layers.length},可多选,空格或逗号分隔;${texts.layersEmptyHint}):`,
172
+ )
173
+ ).trim();
174
+ const picked = parseLayerAnswer(layers, answer);
175
+ if (picked === undefined) {
176
+ process.stderr.write(`${texts.layersInvalid}\n`);
177
+ continue;
178
+ }
179
+ const conflicts = findLayerConflicts(picked, catalogue);
180
+ if (conflicts.length === 0) return picked;
181
+ for (const [left, right] of conflicts) {
182
+ process.stderr.write(
183
+ `${texts.layersConflict}:${labels.get(left) ?? left} 与 ${labels.get(right) ?? right} 不能共存\n`,
184
+ );
185
+ }
186
+ process.stderr.write(`${texts.layersConflictHint}:`);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * 目标目录已存在且有内容时问怎么处置。
192
+ *
193
+ * 三项的顺序是「保留 → 清空 → 取消」,但**回车取的是取消**:前面的选项会改动
194
+ * 磁盘上的既有文件,回车只是「没想好」,不该替使用者做破坏性决定。
195
+ */
196
+ export async function askExisting(
197
+ io: PromptIo,
198
+ texts: PromptTexts,
199
+ ): Promise<ExistingTarget> {
200
+ const options: readonly TemplateOption[] = [
201
+ {
202
+ id: "ignore",
203
+ label: texts.existingIgnore,
204
+ hint: texts.existingIgnoreHint,
205
+ },
206
+ { id: "clear", label: texts.existingClear, hint: texts.existingClearHint },
207
+ {
208
+ id: "cancel",
209
+ label: texts.existingCancel,
210
+ hint: texts.existingCancelHint,
211
+ },
212
+ ];
83
213
  const lines = options
84
214
  .map((option, index) => ` ${index + 1}. ${option.label} —— ${option.hint}`)
85
215
  .join("\n");
86
- process.stderr.write(`${label}:\n${lines}\n`);
216
+ process.stderr.write(`${texts.promptExisting}:\n${lines}\n`);
87
217
  for (;;) {
88
218
  const answer = (
89
- await io.question(`选择(1-${options.length},回车取 1):`)
219
+ await io.question(`选择(1-${options.length},回车取消):`)
90
220
  ).trim();
221
+ if (answer === "") return "cancel";
91
222
  const picked = pickOption(options, answer);
92
- if (picked !== undefined) return picked;
93
- process.stderr.write(`${invalidText}\n`);
223
+ if (picked !== undefined) return picked as ExistingTarget;
224
+ process.stderr.write(`${texts.promptExisting}:${answer}\n`);
94
225
  }
95
226
  }
96
227
 
@@ -115,3 +246,71 @@ export function pickOption(
115
246
  }
116
247
  return undefined;
117
248
  }
249
+
250
+ /**
251
+ * 把一次多选输入解析成层 id 列表,保序去重;有认不出的编号或名字时返回 undefined。
252
+ *
253
+ * 认两种写法(序号与 id)是为了脚本与手敲都顺手;认空格、半角逗号与全角逗号是因为
254
+ * 中文输入法下逗号常常是全角的,为这个报错只会让人困惑。
255
+ *
256
+ * 空输入是空集而**不是**错误:不选任何层就是「只用骨架」,那是最常见的形态。
257
+ */
258
+ export function parseLayerAnswer(
259
+ layers: readonly TemplateOption[],
260
+ answer: string,
261
+ ): string[] | undefined {
262
+ const tokens = answer
263
+ .split(/[\s,,]+/)
264
+ .map((token) => token.trim())
265
+ .filter((token) => token !== "");
266
+ if (tokens.length === 0) return [];
267
+ const out: string[] = [];
268
+ for (const token of tokens) {
269
+ const picked = pickOptionStrict(layers, token);
270
+ if (picked === undefined) return undefined;
271
+ if (!out.includes(picked)) out.push(picked);
272
+ }
273
+ return out;
274
+ }
275
+
276
+ /**
277
+ * 从选中的层里找出互斥对。
278
+ *
279
+ * 只看「选中的这两层彼此排斥」,不看谁先声明的:`excludes` 单边声明即生效,
280
+ * 因此两边都写了也只算一对。
281
+ */
282
+ export function findLayerConflicts(
283
+ selected: readonly string[],
284
+ catalogue: readonly TemplateOption[],
285
+ ): LayerConflict[] {
286
+ const chosen = new Set(selected);
287
+ const seen = new Set<string>();
288
+ const out: LayerConflict[] = [];
289
+ for (const option of catalogue) {
290
+ if (!chosen.has(option.id)) continue;
291
+ for (const excluded of option.excludes ?? []) {
292
+ if (!chosen.has(excluded)) continue;
293
+ const key = [option.id, excluded].sort().join("\u0000");
294
+ if (seen.has(key)) continue;
295
+ seen.add(key);
296
+ out.push([option.id, excluded]);
297
+ }
298
+ }
299
+ return out;
300
+ }
301
+
302
+ /** 与 `pickOption` 同义,但不把空输入当首项:多选里空输入是「什么都不选」 */
303
+ function pickOptionStrict(
304
+ options: readonly TemplateOption[],
305
+ token: string,
306
+ ): string | undefined {
307
+ if (token === "") return undefined;
308
+ const index = Number(token);
309
+ if (Number.isInteger(index) && index >= 1 && index <= options.length) {
310
+ return options[index - 1].id;
311
+ }
312
+ for (const option of options) {
313
+ if (option.id === token) return option.id;
314
+ }
315
+ return undefined;
316
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * 生成阶段:查版本 → 写文件 → 可选安装 → 打印后续步骤。
3
+ *
4
+ * 为什么与参数解析分开:这一段对使用者是「正在干活」的进度叙事,对测试是纯副作用,
5
+ * 而参数解析是纯函数。两者放在一个文件里,任何一边的改动都要重新读另一边的代码。
6
+ *
7
+ * 入参自带形状而不复用命令行类型:本文件不需要知道参数是怎么解析出来的,只认得
8
+ * 「要生成什么」。这也让方向保持单向——参数层依赖这里,这里不回头依赖参数层。
9
+ */
10
+ import { resolve as resolvePath } from "node:path";
11
+ import { currentPackageManager, defaultCreateTexts } from "../shared/index.ts";
12
+ import {
13
+ collectDependencyNames,
14
+ generateProject,
15
+ layerBases,
16
+ type ExistingTarget,
17
+ } from "../generate/index.ts";
18
+ import {
19
+ checkEngineCompatibility,
20
+ resolveVersions,
21
+ type TemplateMeta,
22
+ } from "../registry/index.ts";
23
+ import { installProject, renderNextSteps } from "./install.ts";
24
+
25
+ /** 生成阶段需要的信息;`description` 为空串表示产物里不出现该字段 */
26
+ export interface StageOptions {
27
+ readonly meta: TemplateMeta;
28
+ readonly templateRoot: string;
29
+ /** 生成到哪个目录下;缺省为当前目录 */
30
+ readonly dir: string | undefined;
31
+ readonly projectName: string;
32
+ readonly description: string;
33
+ readonly framework: string;
34
+ readonly shell: string;
35
+ readonly sources: string;
36
+ readonly existing: ExistingTarget;
37
+ readonly install: boolean;
38
+ }
39
+
40
+ /**
41
+ * 执行一次生成,返回进程退出码。
42
+ *
43
+ * 版本查询与兼容性判定都排在写盘之前:模板要求的引擎还没发布时,生成出来的工程
44
+ * 装不上,让它失败在「什么都没写」的时刻,而不是在别人的工程里第一次启动时。
45
+ */
46
+ export async function runStage(options: StageOptions): Promise<number> {
47
+ const texts = defaultCreateTexts;
48
+ const manager = currentPackageManager();
49
+ const targetBase = resolvePath(options.dir ?? process.cwd());
50
+ const layout = {
51
+ templateRoot: options.templateRoot,
52
+ targetBase,
53
+ projectName: options.projectName,
54
+ description: options.description,
55
+ framework: options.framework,
56
+ shell: options.shell,
57
+ sources: options.sources,
58
+ resolved: new Map<string, string>(),
59
+ };
60
+
61
+ process.stderr.write(`${texts.stageResolve}\n`);
62
+ const names = collectDependencyNames(layerBases(layout));
63
+ const resolved = await resolveVersions(names);
64
+ if (resolved.size === 0 && names.length > 0) {
65
+ process.stderr.write(`${texts.warnVersionOffline}\n`);
66
+ }
67
+ const incompatible = checkEngineCompatibility(
68
+ options.meta.minEngineVersion,
69
+ resolved,
70
+ );
71
+ if (incompatible !== undefined) {
72
+ process.stderr.write(`${incompatible}\n`);
73
+ return 1;
74
+ }
75
+
76
+ process.stderr.write(`${texts.stageCopy}\n`);
77
+ let result;
78
+ try {
79
+ result = generateProject({
80
+ ...layout,
81
+ existing: options.existing,
82
+ resolved,
83
+ });
84
+ } catch (error) {
85
+ process.stderr.write(`${describe(error)}\n`);
86
+ return 1;
87
+ }
88
+
89
+ let installed = false;
90
+ if (options.install) {
91
+ process.stderr.write(`${texts.stageInstall}\n`);
92
+ installed = await installProject(result.projectDir, manager);
93
+ if (!installed) process.stderr.write(`${texts.warnInstallFailed}\n`);
94
+ }
95
+
96
+ process.stdout.write(
97
+ [
98
+ "",
99
+ `${texts.doneTitle}:${result.projectDir}`,
100
+ `共 ${result.fileCount} 个文件,框架 ${result.framework},壳 ${result.shell},故事源 ${result.sources}`,
101
+ "",
102
+ installed ? "依赖已安装。" : "依赖未安装。",
103
+ renderNextSteps(result.projectDir, manager, texts),
104
+ "",
105
+ ].join("\n"),
106
+ );
107
+ return 0;
108
+ }
109
+
110
+ /** 把异常折成一行提示:只暴露信息,不暴露堆栈 */
111
+ function describe(error: unknown): string {
112
+ return error instanceof Error ? error.message : String(error);
113
+ }
@@ -7,11 +7,16 @@ export {
7
7
  collectDependencyNames,
8
8
  DEFAULT_SOURCES,
9
9
  generateProject,
10
+ isOccupied,
10
11
  layerBases,
11
- pinnedNames,
12
+ projectDirOf,
12
13
  SKELETON_DIR,
13
14
  } from "./project.ts";
14
- export type { GenerateOptions, GenerateResult } from "./project.ts";
15
+ export type {
16
+ ExistingTarget,
17
+ GenerateOptions,
18
+ GenerateResult,
19
+ } from "./project.ts";
15
20
  export {
16
21
  isTextPath,
17
22
  PLACEHOLDERS,