create-eigen-game 0.13.0 → 0.14.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.
- package/README.md +1 -1
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +466 -378
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +119 -108
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -27
- package/dist/src-B8zHulnO.js +836 -0
- package/dist/src-B8zHulnO.js.map +1 -0
- package/package.json +8 -8
- package/src/cli.ts +437 -0
- package/{dist/engine-range.d.ts → src/engine-range.ts} +23 -2
- package/src/index.ts +974 -0
- package/src/link-firebase.ts +70 -0
- package/src/prompt.ts +182 -0
- package/src/summary.ts +86 -0
- package/templates/app-overlay/lib/game/README.md +1 -1
- package/templates/app-overlay/lib/main.dart.template +12 -6
- package/templates/project/README.md +2 -2
- package/templates/project/package.json +3 -3
- package/templates/worker/.dev.vars.example +1 -0
- package/templates/worker/README.md +5 -2
- package/templates/worker/src/module/v1.ts +1 -0
- package/dist/chunk-L7EVXO3Y.js +0 -439
- package/dist/chunk-L7EVXO3Y.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/link-firebase.d.ts +0 -28
- package/dist/prompt.d.ts +0 -79
- package/dist/summary.d.ts +0 -26
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
//#region src/engine-range.ts
|
|
6
|
+
/**
|
|
7
|
+
* Where the engine range emitted into a scaffolded project comes from.
|
|
8
|
+
*
|
|
9
|
+
* Its own module because the two branches below execute in different worlds,
|
|
10
|
+
* one only inside this repository and the other only inside a published tarball,
|
|
11
|
+
* and a function whose published-only path never runs in CI is a function that
|
|
12
|
+
* is not tested. `src/index.ts` uses it; the package `exports` map does not
|
|
13
|
+
* expose it, so it is internal despite being importable by the tests.
|
|
14
|
+
*/
|
|
15
|
+
/** The dependency whose version the scaffolder emits into the generated server. */
|
|
16
|
+
const ENGINE_PACKAGE = "@eigeninteractive/server";
|
|
17
|
+
/**
|
|
18
|
+
* The engine range for a scaffolded project, from this package's own
|
|
19
|
+
* `@eigeninteractive/server` devDependency.
|
|
20
|
+
*
|
|
21
|
+
* That number is not maintained by anyone: it is the version CI compiled the
|
|
22
|
+
* templates against, because `pnpm -r typecheck` runs
|
|
23
|
+
* `tsc -p templates/worker/tsconfig.json` inside this workspace. Emitting
|
|
24
|
+
* anything else would mean shipping templates paired with an engine no build
|
|
25
|
+
* ever saw.
|
|
26
|
+
*
|
|
27
|
+
* It replaced a derivation from this package's OWN version, which was correct
|
|
28
|
+
* only while `create-eigen-game` sat in the `fixed` changesets group, and that
|
|
29
|
+
* membership is what forced an engine-wide release for every scaffolder-only
|
|
30
|
+
* change. Reading the dependency keeps the guarantee and drops the coupling.
|
|
31
|
+
*
|
|
32
|
+
* Two branches, because the same manifest reads differently in the two places
|
|
33
|
+
* this runs:
|
|
34
|
+
*
|
|
35
|
+
* - **Published tarball**: pnpm rewrites `workspace:*` to the exact version
|
|
36
|
+
* while packing, so the manifest already states it (`"0.2.0"`), and no
|
|
37
|
+
* sibling package exists on disk to consult.
|
|
38
|
+
* - **This workspace**: the manifest still says `workspace:*`, which names no
|
|
39
|
+
* version, so the sibling package is read directly.
|
|
40
|
+
*
|
|
41
|
+
* @param declared the raw `@eigeninteractive/server` devDependency value
|
|
42
|
+
* @param readSiblingVersion consulted only for the `workspace:` protocol
|
|
43
|
+
*/
|
|
44
|
+
function engineRange(declared, readSiblingVersion) {
|
|
45
|
+
if (!declared) throw new Error(`create-eigen-game has no ${ENGINE_PACKAGE} devDependency, which is where the engine range it emits comes from`);
|
|
46
|
+
const version = declared.startsWith("workspace:") ? readSiblingVersion() : declared;
|
|
47
|
+
if (!version) throw new Error(`create-eigen-game declares ${ENGINE_PACKAGE} as "${declared}" but the workspace package could not be read, so there is no engine version to emit`);
|
|
48
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) throw new Error(`expected an exact ${ENGINE_PACKAGE} version to build a range from, got "${version}"`);
|
|
49
|
+
return `^${version}`;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/link-firebase.ts
|
|
53
|
+
/**
|
|
54
|
+
* Reads the two deployment values that exist only once a Firebase project
|
|
55
|
+
* does, so the closing summary can name what is still missing.
|
|
56
|
+
*
|
|
57
|
+
* Reading only. `configure_firebase` writes them, into `wrangler.jsonc` and
|
|
58
|
+
* `app-config.json`, because it is also what a project runs later when
|
|
59
|
+
* reconfiguring, and one implementation is what keeps the scaffold-time path
|
|
60
|
+
* and the re-run path identical. This module exists so the summary can tell
|
|
61
|
+
* the difference between "filled in" and "still owed", not to do it again.
|
|
62
|
+
*
|
|
63
|
+
* Its own module for the same reason `summary.ts` is: `index.ts` runs a
|
|
64
|
+
* scaffold, and this is worth asserting without one. The package `exports` map
|
|
65
|
+
* does not expose it.
|
|
66
|
+
*/
|
|
67
|
+
function readJson(path) {
|
|
68
|
+
if (!existsSync(path)) return void 0;
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
71
|
+
} catch {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The project FlutterFire recorded for the Dart output this app uses.
|
|
77
|
+
*
|
|
78
|
+
* `firebase.json` is FlutterFire's own record rather than something this
|
|
79
|
+
* scaffold writes, so it is read defensively: a shape that does not match is
|
|
80
|
+
* treated as "not configured", which is the same answer as a run that never
|
|
81
|
+
* happened.
|
|
82
|
+
*/
|
|
83
|
+
function configuredProject(appRoot) {
|
|
84
|
+
const projectId = readJson(resolve(appRoot, "firebase.json"))?.flutter?.platforms?.dart?.["lib/firebase_options.dart"]?.projectId;
|
|
85
|
+
return typeof projectId === "string" && projectId !== "" ? projectId : null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The OAuth web client id that `configure_firebase` copied into
|
|
89
|
+
* `app-config.json`, when there was one to copy.
|
|
90
|
+
*
|
|
91
|
+
* Empty is an ordinary outcome rather than a failure. Firebase creates that
|
|
92
|
+
* client when the Google sign-in provider is enabled, which is a console
|
|
93
|
+
* action no CLI can perform, so a project that has never had it enabled has
|
|
94
|
+
* nothing to offer here.
|
|
95
|
+
*/
|
|
96
|
+
function configuredWebClientId(appRoot) {
|
|
97
|
+
const value = readJson(resolve(appRoot, "app-config.json"))?.GOOGLE_WEB_CLIENT_ID;
|
|
98
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
99
|
+
}
|
|
100
|
+
/** What the Firebase step settled, for the summary to report. */
|
|
101
|
+
function readFirebaseLink(root) {
|
|
102
|
+
const appRoot = resolve(root, "app");
|
|
103
|
+
return {
|
|
104
|
+
projectId: configuredProject(appRoot),
|
|
105
|
+
googleWebClientId: configuredWebClientId(appRoot)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/index.ts
|
|
110
|
+
/**
|
|
111
|
+
* Gives a terminal that reports no width the width clack would have assumed.
|
|
112
|
+
*
|
|
113
|
+
* A pty can report itself as a TTY and set `columns` to 0. `script` does, and
|
|
114
|
+
* so do some container terminals. clack falls back to 80 for a stream with no
|
|
115
|
+
* `columns` at all, but `0` is a number and satisfies that check, so the width
|
|
116
|
+
* it draws its rules from goes negative and a scaffold dies partway through
|
|
117
|
+
* with `RangeError: Invalid string length`. Supplying the default its check
|
|
118
|
+
* misses is better than giving up the drawing.
|
|
119
|
+
*/
|
|
120
|
+
function normaliseTerminalWidth(stream) {
|
|
121
|
+
if (stream.isTTY === true && (stream.columns ?? 0) < 1) stream.columns = 80;
|
|
122
|
+
}
|
|
123
|
+
/** Runs every step plainly, which is what a library caller gets by default. */
|
|
124
|
+
const plainReporter = {
|
|
125
|
+
step: (_label, body) => body(),
|
|
126
|
+
handOver: (_label, body) => body(),
|
|
127
|
+
emit: (output) => process.stdout.write(output),
|
|
128
|
+
warn: (message) => console.warn(`create-eigen-game: ${message}`),
|
|
129
|
+
interactive: true
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* The runner a scaffold uses when the caller has not supplied one: it asks the
|
|
133
|
+
* reporter whether the terminal is spoken for.
|
|
134
|
+
*
|
|
135
|
+
* `interactive` inherits, which is the only way FlutterFire can put a question
|
|
136
|
+
* on screen and read the answer. Otherwise both streams are captured and
|
|
137
|
+
* handed to the reporter: on success so it can discard them, and on failure
|
|
138
|
+
* so the reason survives.
|
|
139
|
+
*/
|
|
140
|
+
function capturingRunner(reporter) {
|
|
141
|
+
return (command, args, cwd) => {
|
|
142
|
+
if (reporter.interactive) {
|
|
143
|
+
execFileSync(command, args, {
|
|
144
|
+
cwd,
|
|
145
|
+
stdio: "inherit"
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
reporter.emit(execFileSync(command, args, {
|
|
151
|
+
cwd,
|
|
152
|
+
encoding: "utf8",
|
|
153
|
+
stdio: [
|
|
154
|
+
"ignore",
|
|
155
|
+
"pipe",
|
|
156
|
+
"pipe"
|
|
157
|
+
]
|
|
158
|
+
}));
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const { stdout, stderr } = error;
|
|
161
|
+
reporter.emit(`${stdout ?? ""}${stderr ?? ""}`);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
167
|
+
const templatesRoot = resolve(packageRoot, "templates");
|
|
168
|
+
/**
|
|
169
|
+
* The engine range emitted into a scaffolded project's package.json.
|
|
170
|
+
*
|
|
171
|
+
* Read from this package's `@eigeninteractive/server` devDependency rather than
|
|
172
|
+
* from its own version. See `engine-range.ts` for why that is the version the
|
|
173
|
+
* templates are known to compile against, and what it replaced.
|
|
174
|
+
*/
|
|
175
|
+
const engineVersion = engineRange(JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8")).devDependencies?.[ENGINE_PACKAGE], () => {
|
|
176
|
+
const sibling = resolve(packageRoot, "../server/package.json");
|
|
177
|
+
return existsSync(sibling) ? JSON.parse(readFileSync(sibling, "utf8")).version : void 0;
|
|
178
|
+
});
|
|
179
|
+
/**
|
|
180
|
+
* The Flutter client range installed into the app half.
|
|
181
|
+
*
|
|
182
|
+
* A stated version, not a derived or resolved one. The Dart templates import
|
|
183
|
+
* `package:eigen_flutter/eigen_flutter.dart` and
|
|
184
|
+
* `package:eigen_flutter/testing/twin_fixtures.dart` and are written against a
|
|
185
|
+
* specific Dart API. `eigen_flutter` is published and versioned separately from
|
|
186
|
+
* this package even though both now live in the platform monorepo, so a source
|
|
187
|
+
* location cannot compute which released version the template should request.
|
|
188
|
+
* Only compiling a scaffolded app establishes it, which the platform gate does
|
|
189
|
+
* on every change.
|
|
190
|
+
*
|
|
191
|
+
* This briefly resolved from pub.dev instead: "the newest `eigen_flutter` whose
|
|
192
|
+
* own `eigen_api` constraint targets the engine line being scaffolded". That
|
|
193
|
+
* predicate is wrong. The `eigen_api` constraint describes the WIRE the integration
|
|
194
|
+
* speaks, not the Dart API these templates call, and the two move
|
|
195
|
+
* independently: a future `eigen_flutter` may legitimately keep `eigen_api:
|
|
196
|
+
* ^0.2.0` while renaming everything the templates touch. It would have been
|
|
197
|
+
* selected, and the generated app would not compile.
|
|
198
|
+
*
|
|
199
|
+
* A caret RANGE rather than an exact version, so it still improves without a
|
|
200
|
+
* republish: `flutter pub add eigen_flutter@^0.4.0` already picks the newest
|
|
201
|
+
* 0.4.x at scaffold time, which is the part the pub.dev lookup was duplicating.
|
|
202
|
+
* What it deliberately cannot do is cross to 0.5.x, the one move that needs a
|
|
203
|
+
* human to confirm the templates still compile.
|
|
204
|
+
*
|
|
205
|
+
* Staleness is therefore a failing check rather than a broken scaffold: this is
|
|
206
|
+
* only ever a release behind, never wrong.
|
|
207
|
+
*
|
|
208
|
+
* Raised to 0.4.0 with the engine's session-snapshot wire. This is the one kind
|
|
209
|
+
* of bump that is not merely an improvement: a scaffold writes both halves, and
|
|
210
|
+
* the worker half is this repository's own line, so the client integration has to
|
|
211
|
+
* speak that line's socket. `eigen_flutter` 0.4.0 is the first that does, and
|
|
212
|
+
* every 0.3.x pins `eigen_api: ^0.2.0`, which cannot read a 0.3.x engine at all.
|
|
213
|
+
* A scaffold pinning 0.3.x would therefore resolve, compile, and then fail to
|
|
214
|
+
* render a game, which is the worst of the three.
|
|
215
|
+
*
|
|
216
|
+
* Raised to 0.4.1 for `EngineConfig.authDomain`, which the generated
|
|
217
|
+
* `main.dart` passes. Additive, so pre-1.0 it is a patch and `^0.4.0` would
|
|
218
|
+
* still resolve it, but `^0.4.0` also still resolves 0.4.0, where the parameter
|
|
219
|
+
* does not exist and the scaffold would not compile. The floor is what makes
|
|
220
|
+
* the generated code and the installed shell agree.
|
|
221
|
+
*
|
|
222
|
+
* Raised to 0.6.0 with the engine's opaque pagination cursors, and this is the
|
|
223
|
+
* same *required* kind of bump the 0.4.0 note describes rather than an
|
|
224
|
+
* improvement. The 0.4.x engine line takes `cursor` as an opaque string and
|
|
225
|
+
* returns `nextCursor` on every paged response; `eigen_flutter` 0.6.0 is the
|
|
226
|
+
* first integration that pins `eigen_api: ^0.4.0` and can read it. Everything below
|
|
227
|
+
* that pins `eigen_api: ^0.3.0` or older, so a scaffold left on `^0.4.1` would
|
|
228
|
+
* install the engine's current line beside a shell that cannot read a single
|
|
229
|
+
* paged list from it: it resolves, it compiles, and then the lobby and history
|
|
230
|
+
* are empty. 0.5.0 is skipped for the same reason, being a web-design release
|
|
231
|
+
* that still speaks the 0.3.x wire.
|
|
232
|
+
*
|
|
233
|
+
* Raised to 0.7.0 for the 0.5.x engine line and the `PlayerLimits` client API.
|
|
234
|
+
* vNext later simplified the wire again: versions are a contiguous prefix and
|
|
235
|
+
* generic mutation identities are gone. The next Flutter release must raise
|
|
236
|
+
* this floor before the corresponding scaffolder is published.
|
|
237
|
+
*
|
|
238
|
+
* Note that this floor and the engine range are raised in one commit on
|
|
239
|
+
* purpose. The scaffolder writes both halves, and `updateInternalDependencies`
|
|
240
|
+
* republishes this package whenever the engine version moves, so between an
|
|
241
|
+
* engine line crossing and this line moving there is a published scaffolder
|
|
242
|
+
* that pairs a new worker with an old shell. That window is real and this is
|
|
243
|
+
* what closes it.
|
|
244
|
+
*
|
|
245
|
+
* The 0.7.0 raise is the exception that shows where that promise cannot hold. A
|
|
246
|
+
* floor names a *published* `eigen_flutter`, and `eigen_flutter` publishes at the
|
|
247
|
+
* end of the release chain, after the npm packages this scaffolder ships with --
|
|
248
|
+
* so at the moment the engine crossed to 0.5.x there was no 0.7.0 to point at,
|
|
249
|
+
* and `create-eigen-game@0.12.0` went out pairing a `^0.5.0` worker with a 0.6.0
|
|
250
|
+
* integration. A Flutter line move therefore costs a follow-up scaffolder patch, and
|
|
251
|
+
* `scripts/scaffold-e2e.mjs` is what refuses to let it be forgotten: it resolves
|
|
252
|
+
* both halves for real and compares the wire lines they land on.
|
|
253
|
+
*/
|
|
254
|
+
const flutterClientVersion = "^0.9.0";
|
|
255
|
+
/** Complete first-party app shell installed by the standard scaffold. */
|
|
256
|
+
const flutterShellVersion = "^0.1.0";
|
|
257
|
+
/** Optional Firebase adapter installed by the standard app scaffold. */
|
|
258
|
+
const firebaseAdapterVersion = "^0.2.0";
|
|
259
|
+
/** Development-only contract compiler installed into the generated app. */
|
|
260
|
+
const dartCodegenVersion = "^0.1.0";
|
|
261
|
+
const gameSlug = (value) => {
|
|
262
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) throw new Error("destination directory name must be a lowercase kebab-case slug, for example my-game");
|
|
263
|
+
return value;
|
|
264
|
+
};
|
|
265
|
+
const dartName = (value) => value.replaceAll("-", "_");
|
|
266
|
+
/**
|
|
267
|
+
* The Android `applicationId`, and iOS bundle id, a scaffold at `directory`
|
|
268
|
+
* will produce, so the CLI can show it rather than describe it.
|
|
269
|
+
*
|
|
270
|
+
* `flutter create --org X --project-name Y` derives `X.Y`. The organization is
|
|
271
|
+
* the *prefix*, not the whole identifier, which is the part worth seeing
|
|
272
|
+
* before answering: Google Play makes it permanent at first upload.
|
|
273
|
+
*/
|
|
274
|
+
function applicationId(directory, org) {
|
|
275
|
+
return `${org?.trim() || "com.example"}.${dartName(gameSlug(basename(resolve(directory))))}`;
|
|
276
|
+
}
|
|
277
|
+
const title = (value) => value.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
278
|
+
const identifier = (value) => {
|
|
279
|
+
const pascal = value.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join("");
|
|
280
|
+
return /^[0-9]/.test(pascal) ? `Game${pascal}` : pascal;
|
|
281
|
+
};
|
|
282
|
+
function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
|
|
283
|
+
if (userAgent?.startsWith("pnpm/")) return "pnpm";
|
|
284
|
+
if (userAgent?.startsWith("npm/")) return "npm";
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Substitutes the scaffold's tokens, of which there are two deliberate kinds.
|
|
288
|
+
*
|
|
289
|
+
* `{{BRACED}}` for values that have no valid example: a package manager,
|
|
290
|
+
* an engine version, an Android package id. They appear only in manifests,
|
|
291
|
+
* READMEs, workflows and the `Appfile`: files nothing compiles.
|
|
292
|
+
*
|
|
293
|
+
* Bare words (`ExampleGame`, `example-game`, …) for names a real example game
|
|
294
|
+
* genuinely has. This is what keeps `templates/worker` and the Dart templates
|
|
295
|
+
* VALID, COMPILING SOURCE rather than text with holes in it, which the rest of
|
|
296
|
+
* this package leans on hard: `typecheck` runs `tsc` over `templates/worker`,
|
|
297
|
+
* the tests `import` its game module directly and build a real contract from
|
|
298
|
+
* it, and the `scaffold` job compiles the Dart templates. Bracing these would
|
|
299
|
+
* trade all of that for a marker.
|
|
300
|
+
*
|
|
301
|
+
* The cost, and the one rule for template authors: a template file cannot
|
|
302
|
+
* contain the literal string `example-game`, `Example Game`, `ExampleGame` or
|
|
303
|
+
* `example_game` and keep it; every occurrence is rewritten, in prose and in
|
|
304
|
+
* code alike. Write around it, or brace a new token instead.
|
|
305
|
+
*
|
|
306
|
+
* Note this means the set of files carrying a token changes whenever someone
|
|
307
|
+
* writes one into an existing file. Nothing needs to track that: `renderTree`
|
|
308
|
+
* decides what to rewrite from the bytes, not from a list of paths.
|
|
309
|
+
*/
|
|
310
|
+
function render(contents, name, manager, packageId) {
|
|
311
|
+
return [
|
|
312
|
+
["@game/example-game-server", `@game/${name}-server`],
|
|
313
|
+
["{{PACKAGE_MANAGER}}", manager],
|
|
314
|
+
["{{PACKAGE_ID}}", packageId],
|
|
315
|
+
["{{ENGINE_VERSION}}", engineVersion],
|
|
316
|
+
["ExampleGame", identifier(name)],
|
|
317
|
+
["Example Game", title(name)],
|
|
318
|
+
["example_game", dartName(name)],
|
|
319
|
+
["example-game", name]
|
|
320
|
+
].reduce((result, [from, to]) => result.replaceAll(from, to), contents);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* True when the bytes are valid UTF-8.
|
|
324
|
+
*
|
|
325
|
+
* This is not a guess at whether a file is "binary"; it is the precondition
|
|
326
|
+
* of the operation. Token substitution is `String.replaceAll`, so a file that
|
|
327
|
+
* cannot be decoded cannot take part, and one that can is safe to rewrite.
|
|
328
|
+
*
|
|
329
|
+
* Deliberately not an extension list. A DENY list (`.png`, `.jar`, …) fails
|
|
330
|
+
* silently in the dangerous direction: the day someone adds a `.webp`, it is
|
|
331
|
+
* corrupted with no error. An ALLOW list fails the safe way but fits this tree
|
|
332
|
+
* badly: `Gemfile`, `Fastfile`, `.nvmrc`, `.fvmrc` and `.ruby-version` all
|
|
333
|
+
* need rendering and none carry a usable extension, so it would have to be a
|
|
334
|
+
* list of extensions *and* a list of bare filenames, both drifting.
|
|
335
|
+
*
|
|
336
|
+
* `.template` is not the signal either, despite the name: it marks files a
|
|
337
|
+
* language server must not claim before they have an enclosing package. Five
|
|
338
|
+
* of them contain no token at all, and nine token-bearing files are not
|
|
339
|
+
* marked, so the two concepts are orthogonal here.
|
|
340
|
+
*
|
|
341
|
+
* `renderTree`'s test pins the resulting classification for the whole tree, so
|
|
342
|
+
* this stays a reviewed decision rather than an invisible one.
|
|
343
|
+
*/
|
|
344
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
345
|
+
function decodeUtf8(bytes) {
|
|
346
|
+
try {
|
|
347
|
+
return utf8.decode(bytes);
|
|
348
|
+
} catch {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function renderTree(source, destination, name, manager, packageId) {
|
|
353
|
+
cpSync(source, destination, { recursive: true });
|
|
354
|
+
const visit = (directory) => {
|
|
355
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
356
|
+
const templatePath = resolve(directory, entry.name);
|
|
357
|
+
if (entry.isDirectory()) {
|
|
358
|
+
visit(templatePath);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (!entry.isFile()) continue;
|
|
362
|
+
const rendered = resolve(destination, relative(source, templatePath));
|
|
363
|
+
const text = decodeUtf8(readFileSync(templatePath));
|
|
364
|
+
if (text !== void 0) writeFileSync(rendered, render(text, name, manager, packageId));
|
|
365
|
+
if (entry.name.endsWith(".template")) renameSync(rendered, rendered.slice(0, -9));
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
visit(source);
|
|
369
|
+
const packagedGitignore = resolve(templatesRoot, "scaffold", `${basename(source)}.gitignore`);
|
|
370
|
+
if (existsSync(packagedGitignore)) writeFileSync(resolve(destination, ".gitignore"), render(readFileSync(packagedGitignore, "utf8"), name, manager, packageId));
|
|
371
|
+
}
|
|
372
|
+
function packageCommand(manager, operation) {
|
|
373
|
+
if (operation === "install") return [manager, ["install"]];
|
|
374
|
+
return [manager, ["run", operation]];
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Appends `block` to a file this scaffolder does not own, as its own paragraph.
|
|
378
|
+
*
|
|
379
|
+
* **Append, never insert.** That is the rule the three callers below follow, and
|
|
380
|
+
* it is what keeps them from competing with `flutter create` and `flutterfire
|
|
381
|
+
* configure` for position in the same files. A pure append cannot collide with
|
|
382
|
+
* whatever those tools write above it; the one edit here that ever needed a
|
|
383
|
+
* position, prepending a Kotlin `import`, was broken by an AGP upgrade and has
|
|
384
|
+
* since been rewritten away. See MAINTAINERS.md for the whole doctrine, including
|
|
385
|
+
* why FlutterFire's `// START:`/`// END:` marker comments are deliberately not
|
|
386
|
+
* copied.
|
|
387
|
+
*
|
|
388
|
+
* `contents` is passed in rather than read here because every caller has already
|
|
389
|
+
* read the file to decide whether its block is present, and two of them assert
|
|
390
|
+
* on the shape of what they found. This owns the whitespace and nothing else.
|
|
391
|
+
*
|
|
392
|
+
* That is worth one function because it was previously written three times with
|
|
393
|
+
* two different answers, so the desugaring block got a blank line before it and
|
|
394
|
+
* the other two did not, and the two Gradle blocks ended up flush against each
|
|
395
|
+
* other in every generated project. Separation belongs to the append, so the
|
|
396
|
+
* block constants carry no leading or trailing newline of their own.
|
|
397
|
+
*/
|
|
398
|
+
function appendBlock(path, contents, block) {
|
|
399
|
+
appendFileSync(path, `${contents.endsWith("\n") ? "" : "\n"}\n${block}\n`);
|
|
400
|
+
}
|
|
401
|
+
const androidDesugaring = `// flutter_local_notifications requires desugaring in the application module.
|
|
402
|
+
// A library plugin cannot enable this compiler setting transitively.
|
|
403
|
+
android {
|
|
404
|
+
compileOptions {
|
|
405
|
+
isCoreLibraryDesugaringEnabled = true
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
dependencies {
|
|
410
|
+
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
|
411
|
+
}`;
|
|
412
|
+
function enableAndroidCoreLibraryDesugaring(appRoot) {
|
|
413
|
+
const gradlePath = resolve(appRoot, "android/app/build.gradle.kts");
|
|
414
|
+
const gradle = readFileSync(gradlePath, "utf8");
|
|
415
|
+
if (!gradle.includes("id(\"com.android.application\")")) throw new Error(`Flutter created an unexpected Android application build file: ${gradlePath}`);
|
|
416
|
+
const settingExists = gradle.includes("isCoreLibraryDesugaringEnabled");
|
|
417
|
+
const dependencyExists = gradle.includes("coreLibraryDesugaring(");
|
|
418
|
+
if (settingExists && dependencyExists) return;
|
|
419
|
+
if (settingExists || dependencyExists) throw new Error(`Flutter created an incomplete Android core library desugaring configuration: ${gradlePath}`);
|
|
420
|
+
appendBlock(gradlePath, gradle, androidDesugaring);
|
|
421
|
+
}
|
|
422
|
+
const androidReleaseSigning = `val releaseKeyProperties: Map<String, String> =
|
|
423
|
+
rootProject.file("key.properties")
|
|
424
|
+
.takeIf { it.exists() }
|
|
425
|
+
?.readLines()
|
|
426
|
+
?.map { it.trim() }
|
|
427
|
+
?.filter { it.isNotEmpty() && !it.startsWith("#") && it.contains("=") }
|
|
428
|
+
?.associate { it.substringBefore("=").trim() to it.substringAfter("=").trim() }
|
|
429
|
+
.orEmpty()
|
|
430
|
+
|
|
431
|
+
android {
|
|
432
|
+
signingConfigs {
|
|
433
|
+
create("release") {
|
|
434
|
+
storeFile = releaseKeyProperties["storeFile"]?.let { file(it) }
|
|
435
|
+
storePassword = releaseKeyProperties["storePassword"]
|
|
436
|
+
keyAlias = releaseKeyProperties["keyAlias"]
|
|
437
|
+
keyPassword = releaseKeyProperties["keyPassword"]
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
buildTypes {
|
|
441
|
+
release {
|
|
442
|
+
signingConfig = if (releaseKeyProperties["storeFile"] != null)
|
|
443
|
+
signingConfigs.getByName("release")
|
|
444
|
+
else
|
|
445
|
+
signingConfigs.getByName("debug")
|
|
446
|
+
isMinifyEnabled = true
|
|
447
|
+
isShrinkResources = true
|
|
448
|
+
proguardFiles(
|
|
449
|
+
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
450
|
+
"proguard-rules.pro",
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}`;
|
|
455
|
+
function enableAndroidReleaseSigning(appRoot) {
|
|
456
|
+
const gradlePath = resolve(appRoot, "android/app/build.gradle.kts");
|
|
457
|
+
const gradle = readFileSync(gradlePath, "utf8");
|
|
458
|
+
if (gradle.includes("releaseKeyProperties")) return;
|
|
459
|
+
appendBlock(gradlePath, gradle, androidReleaseSigning);
|
|
460
|
+
}
|
|
461
|
+
const launcherIconsAndSplashConfig = `flutter_launcher_icons:
|
|
462
|
+
android: true
|
|
463
|
+
web:
|
|
464
|
+
generate: true
|
|
465
|
+
image_path: "assets/icon/icon.png"
|
|
466
|
+
image_path: "assets/icon/icon.png"
|
|
467
|
+
adaptive_icon_background: "#1B1E24"
|
|
468
|
+
adaptive_icon_foreground: "assets/icon/icon_foreground.png"
|
|
469
|
+
min_sdk_android: 21
|
|
470
|
+
|
|
471
|
+
flutter_native_splash:
|
|
472
|
+
color: "#F4F1EA"
|
|
473
|
+
color_dark: "#1B1E24"
|
|
474
|
+
image: assets/icon/splash.png
|
|
475
|
+
image_dark: assets/icon/splash_dark.png
|
|
476
|
+
fullscreen: true
|
|
477
|
+
android_12:
|
|
478
|
+
color: "#F4F1EA"
|
|
479
|
+
color_dark: "#1B1E24"
|
|
480
|
+
image: assets/icon/splash.png
|
|
481
|
+
image_dark: assets/icon/splash_dark.png
|
|
482
|
+
icon_background_color: "#F4F1EA"
|
|
483
|
+
icon_background_color_dark: "#1B1E24"
|
|
484
|
+
web: true`;
|
|
485
|
+
function configureLauncherIconsAndSplash(appRoot) {
|
|
486
|
+
const pubspecPath = resolve(appRoot, "pubspec.yaml");
|
|
487
|
+
const pubspec = readFileSync(pubspecPath, "utf8");
|
|
488
|
+
if (pubspec.includes("flutter_launcher_icons:")) return;
|
|
489
|
+
appendBlock(pubspecPath, pubspec, launcherIconsAndSplashConfig);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Adds the GitHub Actions workflows to an existing generated project.
|
|
493
|
+
*
|
|
494
|
+
* The companion to `ci: false`: a game can be built and played locally for as
|
|
495
|
+
* long as its author likes, then gain the same PR gate and signed release
|
|
496
|
+
* pipeline a `--ci` scaffold would have produced.
|
|
497
|
+
*/
|
|
498
|
+
function addContinuousIntegration(options) {
|
|
499
|
+
const root = resolve(options.directory);
|
|
500
|
+
const manifestPath = resolve(root, "package.json");
|
|
501
|
+
if (!existsSync(manifestPath) || !existsSync(resolve(root, "server")) || !existsSync(resolve(root, "app"))) throw new Error(`not an EigenInteractive game project (expected package.json, server/ and app/): ${root}`);
|
|
502
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
503
|
+
const name = gameSlug(manifest.name ?? basename(root));
|
|
504
|
+
const recorded = manifest.scripts?.contract?.includes("pnpm ") ? "pnpm" : manifest.scripts?.contract?.includes("npm ") ? "npm" : void 0;
|
|
505
|
+
const manager = options.packageManager ?? recorded ?? detectPackageManager() ?? "pnpm";
|
|
506
|
+
const appfile = resolve(root, "app/fastlane/Appfile");
|
|
507
|
+
const packageId = existsSync(appfile) ? /package_name\("([^"]+)"\)/.exec(readFileSync(appfile, "utf8"))?.[1] ?? `com.example.${dartName(name)}` : `com.example.${dartName(name)}`;
|
|
508
|
+
const workflows = resolve(root, ".github/workflows");
|
|
509
|
+
const existing = existsSync(workflows) ? readdirSync(workflows).filter((f) => f === "checks.yml" || f === "release.yml") : [];
|
|
510
|
+
if (existing.length > 0) throw new Error(`refusing to overwrite existing workflows: ${existing.join(", ")}`);
|
|
511
|
+
renderTree(resolve(templatesRoot, "ci"), root, name, manager, packageId);
|
|
512
|
+
return {
|
|
513
|
+
root,
|
|
514
|
+
files: [".github/workflows/checks.yml", ".github/workflows/release.yml"]
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
const probeCommand = (command, args) => {
|
|
518
|
+
try {
|
|
519
|
+
return {
|
|
520
|
+
ok: true,
|
|
521
|
+
stdout: execFileSync(command, args, {
|
|
522
|
+
encoding: "utf8",
|
|
523
|
+
stdio: [
|
|
524
|
+
"ignore",
|
|
525
|
+
"pipe",
|
|
526
|
+
"ignore"
|
|
527
|
+
]
|
|
528
|
+
})
|
|
529
|
+
};
|
|
530
|
+
} catch {
|
|
531
|
+
return {
|
|
532
|
+
ok: false,
|
|
533
|
+
stdout: ""
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
/**
|
|
538
|
+
* Whether this machine can configure Firebase, asked *before* the scaffold
|
|
539
|
+
* rather than after it.
|
|
540
|
+
*
|
|
541
|
+
* `configure_firebase` runs the same three checks and is the authority on
|
|
542
|
+
* them; this one exists only to move the answer earlier. Learning that
|
|
543
|
+
* `flutterfire` is missing costs nothing here and costs two minutes of Flutter
|
|
544
|
+
* and pub at the far end, which is where the step actually runs.
|
|
545
|
+
*
|
|
546
|
+
* Every problem, not the first one. The checks are independent (two CLIs from
|
|
547
|
+
* two ecosystems, plus a sign-in) so short-circuiting turns one setup into a
|
|
548
|
+
* sequence of runs that each reveal the next missing piece. Reported in the
|
|
549
|
+
* order they have to be fixed in: the CLI, then the sign-in it stores, then
|
|
550
|
+
* the bridge that consumes both.
|
|
551
|
+
*
|
|
552
|
+
* The sign-in check fails open, for the same reason it does there: only an
|
|
553
|
+
* answer that positively reports no accounts counts as "no", so a `firebase`
|
|
554
|
+
* that errors or grows a different output shape is left to the command that
|
|
555
|
+
* actually needs the credentials.
|
|
556
|
+
*/
|
|
557
|
+
function firebaseReadiness(probe = probeCommand) {
|
|
558
|
+
const problems = [];
|
|
559
|
+
if (probe("firebase", ["--version"]).ok) {
|
|
560
|
+
if (signedOut(probe)) problems.push({
|
|
561
|
+
reason: "no Google account is signed in to the Firebase CLI",
|
|
562
|
+
fix: "firebase login"
|
|
563
|
+
});
|
|
564
|
+
} else problems.push({
|
|
565
|
+
reason: "the `firebase` CLI is not installed",
|
|
566
|
+
fix: "curl -sL https://firebase.tools | bash"
|
|
567
|
+
});
|
|
568
|
+
if (!probe("flutterfire", ["--version"]).ok) problems.push({
|
|
569
|
+
reason: "the `flutterfire` CLI is not installed",
|
|
570
|
+
fix: "dart pub global activate flutterfire_cli"
|
|
571
|
+
});
|
|
572
|
+
return problems.length === 0 ? { ready: true } : {
|
|
573
|
+
ready: false,
|
|
574
|
+
problems
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Whether the Firebase CLI holds no credentials.
|
|
579
|
+
*
|
|
580
|
+
* `login:list --json` prints the stored refresh and access tokens along with
|
|
581
|
+
* the account list. Read the shape, count the entries, and never surface the
|
|
582
|
+
* output: not in a warning, not in a captured stream, not on the failure
|
|
583
|
+
* path. That is why this returns a boolean rather than the response.
|
|
584
|
+
*/
|
|
585
|
+
function signedOut(probe) {
|
|
586
|
+
const accounts = probe("firebase", ["login:list", "--json"]);
|
|
587
|
+
if (!accounts.ok) return false;
|
|
588
|
+
try {
|
|
589
|
+
const { result } = JSON.parse(accounts.stdout);
|
|
590
|
+
return Array.isArray(result) && result.length === 0;
|
|
591
|
+
} catch {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Whether the scaffold would land inside a repository that already exists.
|
|
597
|
+
*
|
|
598
|
+
* Asked of the nearest ancestor that exists, because the destination does not
|
|
599
|
+
* yet. The point is not to change what {@link scaffoldGame} does, which already
|
|
600
|
+
* declines to nest a repository, but to stop the CLI asking a question whose
|
|
601
|
+
* answer cannot matter.
|
|
602
|
+
*/
|
|
603
|
+
/**
|
|
604
|
+
* Why this destination cannot be scaffolded into, said as a whole message, or
|
|
605
|
+
* `undefined` when there is nothing in the way.
|
|
606
|
+
*
|
|
607
|
+
* A directory that exists and is *empty* is not in the way, since people make
|
|
608
|
+
* one out of habit, so it is removed and rewritten rather than refused over
|
|
609
|
+
* nothing. Anything with contents in it is refused, and a cloned repository is
|
|
610
|
+
* not an exception: `.git` is in the way like any other entry, and the message
|
|
611
|
+
* names it. Publishing into one would mean parking `.git`, renaming staging
|
|
612
|
+
* over the top and putting it back, with a failure in the middle able to
|
|
613
|
+
* orphan a remote, which is a lot of care to spend on a flow that has a
|
|
614
|
+
* one-line alternative.
|
|
615
|
+
*
|
|
616
|
+
* Refused, and deliberately not offered as a "remove it and continue?"
|
|
617
|
+
* question, which is where several scaffolders in this family go. This one
|
|
618
|
+
* writes ninety-odd files and runs `flutter create`; the cost of getting that
|
|
619
|
+
* answer wrong against a mistyped path has no upper bound, and `rm -rf` is one
|
|
620
|
+
* command that belongs to the person who can see what is in there. Every other
|
|
621
|
+
* question this CLI asks is reversible. This one would not be.
|
|
622
|
+
*
|
|
623
|
+
* Exported so the CLI can ask it before the first question rather than after
|
|
624
|
+
* the last one. {@link scaffoldGame} asks it again, for callers that are not
|
|
625
|
+
* the CLI.
|
|
626
|
+
*/
|
|
627
|
+
function destinationProblem(directory) {
|
|
628
|
+
const root = resolve(directory);
|
|
629
|
+
if (!existsSync(root)) return void 0;
|
|
630
|
+
if (!statSync(root).isDirectory()) return `${root} already exists, and is a file.`;
|
|
631
|
+
const entries = readdirSync(root);
|
|
632
|
+
if (entries.length === 0) return void 0;
|
|
633
|
+
return `${root} already exists, and is not empty: it holds ${entries.slice(0, 3).sort().join(", ")}${entries.length > 3 ? `, and ${entries.length - 3} more` : ""}.\n\nScaffold somewhere else, or clear it out yourself. This will not delete a directory you already had.`;
|
|
634
|
+
}
|
|
635
|
+
function insideWorkTree(directory, probe = probeCommand) {
|
|
636
|
+
let candidate = resolve(directory);
|
|
637
|
+
while (!existsSync(candidate)) {
|
|
638
|
+
const parent = dirname(candidate);
|
|
639
|
+
if (parent === candidate) return false;
|
|
640
|
+
candidate = parent;
|
|
641
|
+
}
|
|
642
|
+
return probe("git", [
|
|
643
|
+
"-C",
|
|
644
|
+
candidate,
|
|
645
|
+
"rev-parse",
|
|
646
|
+
"--is-inside-work-tree"
|
|
647
|
+
]).ok;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Runs the Flutter client's `configure_firebase` against the generated app.
|
|
651
|
+
*
|
|
652
|
+
* The command owns everything about how Firebase is configured: which
|
|
653
|
+
* platforms, which CLIs must exist, whether anyone is signed in, and what to
|
|
654
|
+
* do when no project has been chosen yet. This only decides *when*: before the
|
|
655
|
+
* scaffold commit, so a configured project is committed configured.
|
|
656
|
+
*
|
|
657
|
+
* Not fatal. Every failure here (a missing `flutterfire`, no Google login, a
|
|
658
|
+
* cancelled picker) leaves a complete and usable project that is exactly what
|
|
659
|
+
* a scaffold without `--firebase` produces, so it warns with the command to
|
|
660
|
+
* re-run and lets the commit happen.
|
|
661
|
+
*/
|
|
662
|
+
function configureFirebase(appRoot, project, run, reporter) {
|
|
663
|
+
const placeholder = resolve(appRoot, "lib/firebase_options.dart");
|
|
664
|
+
const parked = `${placeholder}.placeholder`;
|
|
665
|
+
const parking = existsSync(placeholder);
|
|
666
|
+
if (parking) renameSync(placeholder, parked);
|
|
667
|
+
try {
|
|
668
|
+
reporter.step("Preparing the Firebase configurator", () => run("dart", [
|
|
669
|
+
"run",
|
|
670
|
+
"eigen_firebase:configure_firebase",
|
|
671
|
+
"--help"
|
|
672
|
+
], appRoot));
|
|
673
|
+
reporter.handOver("Configuring Firebase", () => run("dart", [
|
|
674
|
+
"run",
|
|
675
|
+
"eigen_firebase:configure_firebase",
|
|
676
|
+
"--worker",
|
|
677
|
+
"../server",
|
|
678
|
+
...typeof project === "string" ? ["--project", project] : []
|
|
679
|
+
], appRoot));
|
|
680
|
+
if (parking) rmSync(parked, { force: true });
|
|
681
|
+
return "configured";
|
|
682
|
+
} catch {
|
|
683
|
+
if (parking) {
|
|
684
|
+
if (existsSync(placeholder)) rmSync(parked, { force: true });
|
|
685
|
+
else renameSync(parked, placeholder);
|
|
686
|
+
}
|
|
687
|
+
reporter.warn("Could not configure Firebase. The project is complete. Run `firebase:configure` yourself, then commit what it writes.");
|
|
688
|
+
return "failed";
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Commits the scaffold, so the first `git diff` is the first game change.
|
|
693
|
+
*
|
|
694
|
+
* This matters more here than in a scaffolder that only writes source. The
|
|
695
|
+
* bootstrap runs `flutter_launcher_icons` and `flutter_native_splash`, which
|
|
696
|
+
* write generated-but-committed files across `android/`, `web/` and
|
|
697
|
+
* `assets/`; without a baseline the first branding change is indistinguishable
|
|
698
|
+
* from the ninety files the scaffolder happened to produce.
|
|
699
|
+
*
|
|
700
|
+
* Nothing here is fatal. The project is complete and valid before this runs,
|
|
701
|
+
* so a missing `git`, or the unconfigured `user.email` that a fresh CI image
|
|
702
|
+
* has, warns and leaves the tree alone rather than discarding a scaffold that
|
|
703
|
+
* took two minutes of Flutter and pub to produce.
|
|
704
|
+
*/
|
|
705
|
+
function initialiseRepository(root, name, run, reporter) {
|
|
706
|
+
try {
|
|
707
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
708
|
+
cwd: root,
|
|
709
|
+
stdio: "ignore"
|
|
710
|
+
});
|
|
711
|
+
return "existing";
|
|
712
|
+
} catch {}
|
|
713
|
+
try {
|
|
714
|
+
run("git", ["init", "--quiet"], root);
|
|
715
|
+
run("git", ["add", "--all"], root);
|
|
716
|
+
} catch {
|
|
717
|
+
reporter.warn("Could not initialise a git repository. The project is complete. Run `git init` yourself when ready.");
|
|
718
|
+
return "failed";
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
run("git", [
|
|
722
|
+
"commit",
|
|
723
|
+
"--quiet",
|
|
724
|
+
"--message",
|
|
725
|
+
`Scaffold ${name}`
|
|
726
|
+
], root);
|
|
727
|
+
} catch {
|
|
728
|
+
reporter.warn(`Initialised a repository and staged the scaffold, but could not commit it. Set \`user.name\` and \`user.email\`, then \`git commit -m "Scaffold ${name}"\`.`);
|
|
729
|
+
return "failed";
|
|
730
|
+
}
|
|
731
|
+
return "committed";
|
|
732
|
+
}
|
|
733
|
+
function scaffoldGame(options) {
|
|
734
|
+
const root = resolve(options.directory);
|
|
735
|
+
const name = gameSlug(basename(root));
|
|
736
|
+
const manager = options.packageManager ?? detectPackageManager() ?? "pnpm";
|
|
737
|
+
const org = options.org?.trim() || "com.example";
|
|
738
|
+
const packageId = applicationId(root, org);
|
|
739
|
+
const bootstrap = options.bootstrap ?? true;
|
|
740
|
+
const reporter = options.reporter ?? plainReporter;
|
|
741
|
+
const run = options.run ?? capturingRunner(reporter);
|
|
742
|
+
const occupied = destinationProblem(root);
|
|
743
|
+
if (occupied !== void 0) throw new Error(occupied);
|
|
744
|
+
if (existsSync(root)) rmdirSync(root);
|
|
745
|
+
mkdirSync(dirname(root), { recursive: true });
|
|
746
|
+
const stagingRoot = mkdtempSync(resolve(dirname(root), `.${basename(root)}-`));
|
|
747
|
+
try {
|
|
748
|
+
const serverRoot = resolve(stagingRoot, "server");
|
|
749
|
+
const appRoot = resolve(stagingRoot, "app");
|
|
750
|
+
renderTree(resolve(templatesRoot, "worker"), serverRoot, name, manager, packageId);
|
|
751
|
+
renderTree(resolve(templatesRoot, "project"), stagingRoot, name, manager, packageId);
|
|
752
|
+
if (options.ci) renderTree(resolve(templatesRoot, "ci"), stagingRoot, name, manager, packageId);
|
|
753
|
+
if (bootstrap) reporter.step("Creating the Flutter app", () => {
|
|
754
|
+
run("flutter", [
|
|
755
|
+
"create",
|
|
756
|
+
"--empty",
|
|
757
|
+
"--platforms",
|
|
758
|
+
"android,web",
|
|
759
|
+
"--project-name",
|
|
760
|
+
dartName(name),
|
|
761
|
+
"--org",
|
|
762
|
+
org,
|
|
763
|
+
appRoot
|
|
764
|
+
], stagingRoot);
|
|
765
|
+
enableAndroidCoreLibraryDesugaring(appRoot);
|
|
766
|
+
enableAndroidReleaseSigning(appRoot);
|
|
767
|
+
});
|
|
768
|
+
else mkdirSync(appRoot, { recursive: true });
|
|
769
|
+
renderTree(resolve(templatesRoot, "app-overlay"), appRoot, name, manager, packageId);
|
|
770
|
+
if (bootstrap) {
|
|
771
|
+
reporter.step("Adding the Flutter packages", () => {
|
|
772
|
+
configureLauncherIconsAndSplash(appRoot);
|
|
773
|
+
run("flutter", [
|
|
774
|
+
"pub",
|
|
775
|
+
"add",
|
|
776
|
+
`eigen_flutter@${flutterClientVersion}`,
|
|
777
|
+
`eigen_shell@${flutterShellVersion}`,
|
|
778
|
+
`eigen_firebase@${firebaseAdapterVersion}`,
|
|
779
|
+
"firebase_core@^4.9.0"
|
|
780
|
+
], appRoot);
|
|
781
|
+
run("flutter", [
|
|
782
|
+
"pub",
|
|
783
|
+
"add",
|
|
784
|
+
`dev:eigen_codegen@${dartCodegenVersion}`,
|
|
785
|
+
"dev:flutter_launcher_icons",
|
|
786
|
+
"dev:flutter_native_splash"
|
|
787
|
+
], appRoot);
|
|
788
|
+
});
|
|
789
|
+
reporter.step("Generating the icons and splash", () => {
|
|
790
|
+
run("dart", ["run", "flutter_launcher_icons"], appRoot);
|
|
791
|
+
run("dart", ["run", "flutter_native_splash:create"], appRoot);
|
|
792
|
+
});
|
|
793
|
+
const [install, installArgs] = packageCommand(manager, "install");
|
|
794
|
+
const [typegen, typegenArgs] = packageCommand(manager, "cf-typegen");
|
|
795
|
+
reporter.step("Installing the Worker packages", () => {
|
|
796
|
+
run(install, installArgs, serverRoot);
|
|
797
|
+
run(typegen, typegenArgs, serverRoot);
|
|
798
|
+
run(install, installArgs, stagingRoot);
|
|
799
|
+
});
|
|
800
|
+
const [contract, contractArgs] = packageCommand(manager, "contract");
|
|
801
|
+
reporter.step("Generating the contract and payloads", () => {
|
|
802
|
+
run(contract, contractArgs, serverRoot);
|
|
803
|
+
run("dart", [
|
|
804
|
+
"run",
|
|
805
|
+
"eigen_codegen:generate_payloads",
|
|
806
|
+
"--contract",
|
|
807
|
+
"../server/game-contract.json",
|
|
808
|
+
"--output",
|
|
809
|
+
"lib/game/generated/payloads.dart",
|
|
810
|
+
"--fixtures-output",
|
|
811
|
+
"test/fixtures"
|
|
812
|
+
], appRoot);
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
renameSync(stagingRoot, root);
|
|
816
|
+
} catch (error) {
|
|
817
|
+
rmSync(stagingRoot, {
|
|
818
|
+
recursive: true,
|
|
819
|
+
force: true
|
|
820
|
+
});
|
|
821
|
+
throw error;
|
|
822
|
+
}
|
|
823
|
+
const firebase = options.firebase ? configureFirebase(resolve(root, "app"), options.firebase, run, reporter) : "skipped";
|
|
824
|
+
const link = firebase === "configured" ? readFirebaseLink(root) : void 0;
|
|
825
|
+
return {
|
|
826
|
+
root,
|
|
827
|
+
name,
|
|
828
|
+
git: options.git ?? bootstrap ? initialiseRepository(root, name, run, reporter) : "skipped",
|
|
829
|
+
firebase,
|
|
830
|
+
link
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
//#endregion
|
|
834
|
+
export { destinationProblem as a, insideWorkTree as c, scaffoldGame as d, decodeUtf8 as i, normaliseTerminalWidth as l, applicationId as n, detectPackageManager as o, capturingRunner as r, firebaseReadiness as s, addContinuousIntegration as t, plainReporter as u };
|
|
835
|
+
|
|
836
|
+
//# sourceMappingURL=src-B8zHulnO.js.map
|