create-eigen-game 0.1.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/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/chunk-BSJVRXI4.js +127 -0
- package/dist/chunk-BSJVRXI4.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +58 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/templates/app-overlay/app-config.json.template +6 -0
- package/templates/app-overlay/lib/firebase_options.dart.template +14 -0
- package/templates/app-overlay/lib/game/README.md +18 -0
- package/templates/app-overlay/lib/game/module.dart.template +29 -0
- package/templates/app-overlay/lib/game/v1/rules.dart.template +79 -0
- package/templates/app-overlay/lib/main.dart.template +39 -0
- package/templates/app-overlay/test/game/twin_fixtures_test.dart.template +28 -0
- package/templates/app-overlay/web/firebase-config.js.template +4 -0
- package/templates/app-overlay/web/firebase-messaging-sw.js.template +15 -0
- package/templates/app-overlay/web/flutter_bootstrap.js.template +19 -0
- package/templates/app-overlay/web/index.html.template +17 -0
- package/templates/project/README.md +104 -0
- package/templates/project/package.json +12 -0
- package/templates/scaffold/project.gitignore +9 -0
- package/templates/scaffold/worker.gitignore +10 -0
- package/templates/worker/.dev.vars.example +4 -0
- package/templates/worker/README.md +50 -0
- package/templates/worker/game-contract.json +109 -0
- package/templates/worker/package.json +51 -0
- package/templates/worker/public/.gitkeep +1 -0
- package/templates/worker/src/index.ts +17 -0
- package/templates/worker/src/module/fixtures/v1/counter.json +31 -0
- package/templates/worker/src/module/index.ts +4 -0
- package/templates/worker/src/module/v1.ts +57 -0
- package/templates/worker/test/twin.spec.ts +4 -0
- package/templates/worker/tsconfig.json +15 -0
- package/templates/worker/worker-configuration.d.ts +14718 -0
- package/templates/worker/wrangler.jsonc +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eigen Interactive
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# create-eigen-game
|
|
2
|
+
|
|
3
|
+
Creates one repository containing an Eigen Cloudflare Worker and Flutter app:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm create eigen-game my-game
|
|
7
|
+
# or
|
|
8
|
+
npm create eigen-game@latest my-game
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The command detects npm or pnpm from the invoking package manager. Pass
|
|
12
|
+
`--package-manager npm|pnpm` to override it and `--org games.example` to set
|
|
13
|
+
the Flutter organization. The destination basename is the one required
|
|
14
|
+
lowercase kebab-case slug. The CLI derives the display name (`My Game`), Dart
|
|
15
|
+
package name (`my_game`), and type prefix (`MyGame`) from `my-game`; there is no
|
|
16
|
+
separate name or prefix input.
|
|
17
|
+
|
|
18
|
+
The generated project has two application-owned halves:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
my-game/
|
|
22
|
+
├── server/ # authoritative TypeScript rules and Cloudflare Worker
|
|
23
|
+
└── app/ # generated Dart payloads and handwritten Flutter presentation
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The scaffolder creates the Flutter shell explicitly for Android and web,
|
|
27
|
+
installs both halves, emits `server/game-contract.json`, and
|
|
28
|
+
generates the initial Dart payload types and typed rules base. Engine
|
|
29
|
+
repositories remain ordinary npm/pub.dev dependencies and are not cloned.
|
|
30
|
+
It also supplies a starter v1 twin fixture, a Vitest runner, a Flutter fixture
|
|
31
|
+
runner, watch-mode rules tests, and generated-contract drift checks. At the
|
|
32
|
+
generated repository root, `pnpm run contract` (or `npm run contract`) emits
|
|
33
|
+
the TypeScript contract and regenerates the Dart payloads in one command;
|
|
34
|
+
`contract:check` checks both artifacts without rewriting them.
|
|
35
|
+
|
|
36
|
+
`pnpm run deploy` (or npm) is similarly whole-project: it builds Flutter web
|
|
37
|
+
into the Worker's Static Assets directory, applies D1 migrations, and deploys
|
|
38
|
+
one same-origin Worker. The Flutter app lives at `/`; the generated native
|
|
39
|
+
install page lives at `/download`. Both Android and web builds read the same
|
|
40
|
+
public values from `app/app-config.json` through Flutter's native
|
|
41
|
+
`--dart-define-from-file` support.
|
|
42
|
+
|
|
43
|
+
## Template architecture
|
|
44
|
+
|
|
45
|
+
`templates/worker` is a valid standalone Cloudflare C3-style Worker template:
|
|
46
|
+
it contains its own `package.json`, `wrangler.jsonc`, TypeScript entry point,
|
|
47
|
+
generated Wrangler types, and game module. The combined Eigen CLI renders that
|
|
48
|
+
same template under `server/`; it does not maintain a second Worker skeleton.
|
|
49
|
+
|
|
50
|
+
`templates/app-overlay` is applied after
|
|
51
|
+
`flutter create --empty --platforms android,web`. It supplies the Firebase
|
|
52
|
+
Messaging service worker, the narrowly scoped Flutter bootstrap that registers
|
|
53
|
+
it before app startup, and one setup command that runs FlutterFire and derives
|
|
54
|
+
the worker's public Firebase configuration from the Web app FlutterFire
|
|
55
|
+
selected. Implementors never copy those identifiers into JavaScript by hand.
|
|
56
|
+
`eigen_flutter` bundles and loads the Cropper.js assets required by
|
|
57
|
+
`image_cropper` on web, so generated apps do not carry their own copy or
|
|
58
|
+
configure `web/index.html`.
|
|
59
|
+
On Android, `eigen_flutter` supplies FID messaging configuration through its
|
|
60
|
+
plugin manifest and native dependency graph; the scaffolder does not edit the
|
|
61
|
+
application manifest or `gradle.properties`. It adds the application-level core
|
|
62
|
+
library desugaring block required by `flutter_local_notifications`, because an
|
|
63
|
+
Android library cannot supply that compiler setting transitively. The CLI then
|
|
64
|
+
uses `flutter pub add` and the public
|
|
65
|
+
`eigen_flutter:generate_payloads` executable instead of editing Flutter's
|
|
66
|
+
generated YAML by hand.
|
|
67
|
+
|
|
68
|
+
The public CLI intentionally has no server-only or app-only modes.
|
|
69
|
+
|
|
70
|
+
The Worker directly depends on both `@eigeninteractive/rules` and
|
|
71
|
+
`@eigeninteractive/server`. Rules is the pure contract the game implements and
|
|
72
|
+
the shared peer identity used by server/kernel/testkit; server is the
|
|
73
|
+
Cloudflare runtime. Server deliberately does not re-export the rules surface.
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
- [Quickstart](https://eigeninteractive.com/docs/getting-started/quickstart)
|
|
78
|
+
- [Manual setup and split repositories](https://eigeninteractive.com/docs/getting-started/manual-setup)
|
|
79
|
+
- [Deploy the web app](https://eigeninteractive.com/docs/ship-it/deploy-the-web-app)
|
|
80
|
+
- [The game contract](https://eigeninteractive.com/docs/build-a-game/the-contract)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
4
|
+
import { basename, dirname, resolve } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
var templatesRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../templates");
|
|
7
|
+
var gameSlug = (value) => {
|
|
8
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
|
|
9
|
+
throw new Error("destination directory name must be a lowercase kebab-case slug, for example my-game");
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
};
|
|
13
|
+
var dartName = (value) => value.replaceAll("-", "_");
|
|
14
|
+
var title = (value) => value.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
15
|
+
var identifier = (value) => {
|
|
16
|
+
const pascal = value.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join("");
|
|
17
|
+
return /^[0-9]/.test(pascal) ? `Game${pascal}` : pascal;
|
|
18
|
+
};
|
|
19
|
+
function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
|
|
20
|
+
if (userAgent?.startsWith("pnpm/")) return "pnpm";
|
|
21
|
+
if (userAgent?.startsWith("npm/")) return "npm";
|
|
22
|
+
return void 0;
|
|
23
|
+
}
|
|
24
|
+
function render(contents, name, manager) {
|
|
25
|
+
const replacements = [
|
|
26
|
+
["@game/example-game-server", `@game/${name}-server`],
|
|
27
|
+
["{{PACKAGE_MANAGER}}", manager],
|
|
28
|
+
["ExampleGame", identifier(name)],
|
|
29
|
+
["Example Game", title(name)],
|
|
30
|
+
["example_game", dartName(name)],
|
|
31
|
+
["example-game", name]
|
|
32
|
+
];
|
|
33
|
+
return replacements.reduce((result, [from, to]) => result.replaceAll(from, to), contents);
|
|
34
|
+
}
|
|
35
|
+
function renderTree(source, destination, name, manager) {
|
|
36
|
+
cpSync(source, destination, { recursive: true });
|
|
37
|
+
const visit = (directory) => {
|
|
38
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
39
|
+
const path = resolve(directory, entry.name);
|
|
40
|
+
if (entry.isDirectory()) visit(path);
|
|
41
|
+
else if (entry.isFile()) {
|
|
42
|
+
writeFileSync(path, render(readFileSync(path, "utf8"), name, manager));
|
|
43
|
+
if (entry.name.endsWith(".template")) {
|
|
44
|
+
renameSync(path, path.slice(0, -".template".length));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
visit(destination);
|
|
50
|
+
const packagedGitignore = resolve(templatesRoot, "scaffold", `${basename(source)}.gitignore`);
|
|
51
|
+
if (existsSync(packagedGitignore)) {
|
|
52
|
+
writeFileSync(resolve(destination, ".gitignore"), render(readFileSync(packagedGitignore, "utf8"), name, manager));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function packageCommand(manager, operation) {
|
|
56
|
+
if (operation === "install") return [manager, ["install"]];
|
|
57
|
+
return [manager, ["run", "contract"]];
|
|
58
|
+
}
|
|
59
|
+
var androidDesugaring = `// flutter_local_notifications requires desugaring in the application module.
|
|
60
|
+
// A library plugin cannot enable this compiler setting transitively.
|
|
61
|
+
android {
|
|
62
|
+
compileOptions {
|
|
63
|
+
isCoreLibraryDesugaringEnabled = true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
dependencies {
|
|
68
|
+
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
|
69
|
+
}`;
|
|
70
|
+
function enableAndroidCoreLibraryDesugaring(appRoot) {
|
|
71
|
+
const gradlePath = resolve(appRoot, "android/app/build.gradle.kts");
|
|
72
|
+
const gradle = readFileSync(gradlePath, "utf8");
|
|
73
|
+
if (!gradle.includes('id("com.android.application")')) {
|
|
74
|
+
throw new Error(`Flutter created an unexpected Android application build file: ${gradlePath}`);
|
|
75
|
+
}
|
|
76
|
+
const settingExists = gradle.includes("isCoreLibraryDesugaringEnabled");
|
|
77
|
+
const dependencyExists = gradle.includes("coreLibraryDesugaring(");
|
|
78
|
+
if (settingExists && dependencyExists) return;
|
|
79
|
+
if (settingExists || dependencyExists) {
|
|
80
|
+
throw new Error(`Flutter created an incomplete Android core library desugaring configuration: ${gradlePath}`);
|
|
81
|
+
}
|
|
82
|
+
appendFileSync(gradlePath, `${gradle.endsWith("\n") ? "\n" : "\n\n"}${androidDesugaring}
|
|
83
|
+
`);
|
|
84
|
+
}
|
|
85
|
+
function scaffoldGame(options) {
|
|
86
|
+
const root = resolve(options.directory);
|
|
87
|
+
const name = gameSlug(basename(root));
|
|
88
|
+
const manager = options.packageManager ?? detectPackageManager() ?? "pnpm";
|
|
89
|
+
const org = options.org?.trim() || "com.example";
|
|
90
|
+
const bootstrap = options.bootstrap ?? true;
|
|
91
|
+
const run = options.run ?? ((command, args, cwd) => execFileSync(command, args, { cwd, stdio: "inherit" }));
|
|
92
|
+
if (existsSync(root)) throw new Error(`target already exists: ${root}`);
|
|
93
|
+
mkdirSync(dirname(root), { recursive: true });
|
|
94
|
+
const stagingRoot = mkdtempSync(resolve(dirname(root), `.${basename(root)}-`));
|
|
95
|
+
try {
|
|
96
|
+
const serverRoot = resolve(stagingRoot, "server");
|
|
97
|
+
const appRoot = resolve(stagingRoot, "app");
|
|
98
|
+
renderTree(resolve(templatesRoot, "worker"), serverRoot, name, manager);
|
|
99
|
+
renderTree(resolve(templatesRoot, "project"), stagingRoot, name, manager);
|
|
100
|
+
if (bootstrap) {
|
|
101
|
+
run("flutter", ["create", "--empty", "--platforms", "android,web", "--project-name", dartName(name), "--org", org, appRoot], stagingRoot);
|
|
102
|
+
enableAndroidCoreLibraryDesugaring(appRoot);
|
|
103
|
+
} else {
|
|
104
|
+
mkdirSync(appRoot, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
renderTree(resolve(templatesRoot, "app-overlay"), appRoot, name, manager);
|
|
107
|
+
if (bootstrap) {
|
|
108
|
+
run("flutter", ["pub", "add", "eigen_flutter@^0.1.0", "firebase_core@^4.9.0", "firebase_messaging@^16.2.2"], appRoot);
|
|
109
|
+
const [install, installArgs] = packageCommand(manager, "install");
|
|
110
|
+
run(install, installArgs, serverRoot);
|
|
111
|
+
const [contract, contractArgs] = packageCommand(manager, "contract");
|
|
112
|
+
run(contract, contractArgs, serverRoot);
|
|
113
|
+
run("dart", ["run", "eigen_flutter:generate_payloads", "--contract", "../server/game-contract.json", "--output", "lib/game/generated/payloads.dart", "--fixtures-output", "test/fixtures"], appRoot);
|
|
114
|
+
}
|
|
115
|
+
renameSync(stagingRoot, root);
|
|
116
|
+
return { root, name };
|
|
117
|
+
} catch (error) {
|
|
118
|
+
rmSync(stagingRoot, { recursive: true, force: true });
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
detectPackageManager,
|
|
125
|
+
scaffoldGame
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=chunk-BSJVRXI4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport type PackageManager = \"npm\" | \"pnpm\";\n\nexport interface ScaffoldOptions {\n directory: string;\n org?: string;\n packageManager?: PackageManager;\n /**\n * Run Flutter, package installation, and contract/payload generation.\n * Kept as a programmatic test seam; the public CLI always bootstraps both\n * halves of the combined project.\n */\n bootstrap?: boolean;\n run?: (command: string, args: string[], cwd: string) => void;\n}\n\nexport interface ScaffoldResult {\n root: string;\n name: string;\n}\n\nconst templatesRoot = resolve(dirname(fileURLToPath(import.meta.url)), \"../templates\");\n\nconst gameSlug = (value: string): string => {\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {\n throw new Error(\"destination directory name must be a lowercase kebab-case slug, for example my-game\");\n }\n return value;\n};\n\nconst dartName = (value: string): string => value.replaceAll(\"-\", \"_\");\n\nconst title = (value: string): string =>\n value\n .split(\"-\")\n .map((part) => `${part[0]?.toUpperCase() ?? \"\"}${part.slice(1)}`)\n .join(\" \");\n\nconst identifier = (value: string): string => {\n const pascal = value\n .split(\"-\")\n .map((part) => `${part[0]?.toUpperCase() ?? \"\"}${part.slice(1)}`)\n .join(\"\");\n return /^[0-9]/.test(pascal) ? `Game${pascal}` : pascal;\n};\n\nexport function detectPackageManager(userAgent = process.env.npm_config_user_agent): PackageManager | undefined {\n if (userAgent?.startsWith(\"pnpm/\")) return \"pnpm\";\n if (userAgent?.startsWith(\"npm/\")) return \"npm\";\n return undefined;\n}\n\nfunction render(contents: string, name: string, manager: PackageManager): string {\n const replacements: ReadonlyArray<readonly [string, string]> = [\n [\"@game/example-game-server\", `@game/${name}-server`],\n [\"{{PACKAGE_MANAGER}}\", manager],\n [\"ExampleGame\", identifier(name)],\n [\"Example Game\", title(name)],\n [\"example_game\", dartName(name)],\n [\"example-game\", name],\n ];\n return replacements.reduce((result, [from, to]) => result.replaceAll(from, to), contents);\n}\n\nfunction renderTree(source: string, destination: string, name: string, manager: PackageManager): void {\n cpSync(source, destination, { recursive: true });\n const visit = (directory: string): void => {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n const path = resolve(directory, entry.name);\n if (entry.isDirectory()) visit(path);\n else if (entry.isFile()) {\n writeFileSync(path, render(readFileSync(path, \"utf8\"), name, manager));\n // Template-only source must not be claimed by language servers before\n // it has an enclosing generated package. Restore the real extension\n // only in the rendered project.\n if (entry.name.endsWith(\".template\")) {\n renameSync(path, path.slice(0, -\".template\".length));\n }\n }\n }\n };\n visit(destination);\n\n // npm deliberately excludes files named `.gitignore` from package\n // tarballs. Their packaged twins live outside the C3 template so a direct\n // Git-based C3 render does not inherit a scaffolder-only helper file.\n const packagedGitignore = resolve(templatesRoot, \"scaffold\", `${basename(source)}.gitignore`);\n if (existsSync(packagedGitignore)) {\n writeFileSync(resolve(destination, \".gitignore\"), render(readFileSync(packagedGitignore, \"utf8\"), name, manager));\n }\n}\n\nfunction packageCommand(manager: PackageManager, operation: \"install\" | \"contract\"): [string, string[]] {\n if (operation === \"install\") return [manager, [\"install\"]];\n return [manager, [\"run\", \"contract\"]];\n}\n\nconst androidDesugaring = `// flutter_local_notifications requires desugaring in the application module.\n// A library plugin cannot enable this compiler setting transitively.\nandroid {\n compileOptions {\n isCoreLibraryDesugaringEnabled = true\n }\n}\n\ndependencies {\n coreLibraryDesugaring(\"com.android.tools:desugar_jdk_libs:2.1.4\")\n}`;\n\nfunction enableAndroidCoreLibraryDesugaring(appRoot: string): void {\n const gradlePath = resolve(appRoot, \"android/app/build.gradle.kts\");\n const gradle = readFileSync(gradlePath, \"utf8\");\n if (!gradle.includes('id(\"com.android.application\")')) {\n throw new Error(`Flutter created an unexpected Android application build file: ${gradlePath}`);\n }\n const settingExists = gradle.includes(\"isCoreLibraryDesugaringEnabled\");\n const dependencyExists = gradle.includes(\"coreLibraryDesugaring(\");\n if (settingExists && dependencyExists) return;\n if (settingExists || dependencyExists) {\n throw new Error(`Flutter created an incomplete Android core library desugaring configuration: ${gradlePath}`);\n }\n appendFileSync(gradlePath, `${gradle.endsWith(\"\\n\") ? \"\\n\" : \"\\n\\n\"}${androidDesugaring}\\n`);\n}\n\nexport function scaffoldGame(options: ScaffoldOptions): ScaffoldResult {\n const root = resolve(options.directory);\n const name = gameSlug(basename(root));\n const manager = options.packageManager ?? detectPackageManager() ?? \"pnpm\";\n const org = options.org?.trim() || \"com.example\";\n const bootstrap = options.bootstrap ?? true;\n const run = options.run ?? ((command, args, cwd) => execFileSync(command, args, { cwd, stdio: \"inherit\" }));\n\n if (existsSync(root)) throw new Error(`target already exists: ${root}`);\n mkdirSync(dirname(root), { recursive: true });\n\n // Build beside the destination and publish it with one rename. Failed\n // subprocesses never leave a half-scaffolded project at the requested path.\n const stagingRoot = mkdtempSync(resolve(dirname(root), `.${basename(root)}-`));\n try {\n const serverRoot = resolve(stagingRoot, \"server\");\n const appRoot = resolve(stagingRoot, \"app\");\n\n renderTree(resolve(templatesRoot, \"worker\"), serverRoot, name, manager);\n renderTree(resolve(templatesRoot, \"project\"), stagingRoot, name, manager);\n\n if (bootstrap) {\n run(\"flutter\", [\"create\", \"--empty\", \"--platforms\", \"android,web\", \"--project-name\", dartName(name), \"--org\", org, appRoot], stagingRoot);\n enableAndroidCoreLibraryDesugaring(appRoot);\n } else {\n mkdirSync(appRoot, { recursive: true });\n }\n renderTree(resolve(templatesRoot, \"app-overlay\"), appRoot, name, manager);\n\n if (bootstrap) {\n run(\"flutter\", [\"pub\", \"add\", \"eigen_flutter@^0.1.0\", \"firebase_core@^4.9.0\", \"firebase_messaging@^16.2.2\"], appRoot);\n const [install, installArgs] = packageCommand(manager, \"install\");\n run(install, installArgs, serverRoot);\n const [contract, contractArgs] = packageCommand(manager, \"contract\");\n run(contract, contractArgs, serverRoot);\n run(\"dart\", [\"run\", \"eigen_flutter:generate_payloads\", \"--contract\", \"../server/game-contract.json\", \"--output\", \"lib/game/generated/payloads.dart\", \"--fixtures-output\", \"test/fixtures\"], appRoot);\n }\n\n renameSync(stagingRoot, root);\n return { root, name };\n } catch (error) {\n rmSync(stagingRoot, { recursive: true, force: true });\n throw error;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
detectPackageManager,
|
|
4
|
+
scaffoldGame
|
|
5
|
+
} from "./chunk-BSJVRXI4.js";
|
|
6
|
+
|
|
7
|
+
// src/cli.ts
|
|
8
|
+
import { parseArgs } from "util";
|
|
9
|
+
var help = `Usage: create-eigen-game <game-slug> [options]
|
|
10
|
+
|
|
11
|
+
Creates one repository containing an Eigen Cloudflare Worker and Flutter app.
|
|
12
|
+
The destination basename is the lowercase kebab-case game slug; the CLI derives
|
|
13
|
+
the display name, Dart package name, and Dart/TypeScript type prefix from it.
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--org <reverse-domain> Android/iOS organization (default: com.example)
|
|
17
|
+
--package-manager <pm> npm or pnpm (defaults to the invoking package manager)
|
|
18
|
+
-h, --help Show this help`;
|
|
19
|
+
function main(args) {
|
|
20
|
+
const { values, positionals } = parseArgs({
|
|
21
|
+
args,
|
|
22
|
+
allowPositionals: true,
|
|
23
|
+
strict: true,
|
|
24
|
+
options: {
|
|
25
|
+
help: { type: "boolean", short: "h" },
|
|
26
|
+
org: { type: "string" },
|
|
27
|
+
"package-manager": { type: "string" }
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
if (values.help) {
|
|
31
|
+
console.log(help);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (positionals.length !== 1) {
|
|
35
|
+
throw new Error(`expected exactly one destination directory
|
|
36
|
+
|
|
37
|
+
${help}`);
|
|
38
|
+
}
|
|
39
|
+
const requestedManager = values["package-manager"];
|
|
40
|
+
if (requestedManager !== void 0 && requestedManager !== "npm" && requestedManager !== "pnpm") {
|
|
41
|
+
throw new Error("--package-manager must be npm or pnpm");
|
|
42
|
+
}
|
|
43
|
+
const packageManager = requestedManager ?? detectPackageManager() ?? "pnpm";
|
|
44
|
+
const result = scaffoldGame({
|
|
45
|
+
directory: positionals[0],
|
|
46
|
+
org: values.org,
|
|
47
|
+
packageManager
|
|
48
|
+
});
|
|
49
|
+
console.log(`Created ${result.name} in ${result.root}`);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
main(process.argv.slice(2));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
55
|
+
console.error(`create-eigen-game: ${message}`);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport { detectPackageManager, type PackageManager, scaffoldGame } from \"./index.js\";\n\nconst help = `Usage: create-eigen-game <game-slug> [options]\n\nCreates one repository containing an Eigen Cloudflare Worker and Flutter app.\nThe destination basename is the lowercase kebab-case game slug; the CLI derives\nthe display name, Dart package name, and Dart/TypeScript type prefix from it.\n\nOptions:\n --org <reverse-domain> Android/iOS organization (default: com.example)\n --package-manager <pm> npm or pnpm (defaults to the invoking package manager)\n -h, --help Show this help`;\n\nfunction main(args: string[]): void {\n const { values, positionals } = parseArgs({\n args,\n allowPositionals: true,\n strict: true,\n options: {\n help: { type: \"boolean\", short: \"h\" },\n org: { type: \"string\" },\n \"package-manager\": { type: \"string\" },\n },\n });\n\n if (values.help) {\n console.log(help);\n return;\n }\n if (positionals.length !== 1) {\n throw new Error(`expected exactly one destination directory\\n\\n${help}`);\n }\n\n const requestedManager = values[\"package-manager\"];\n if (requestedManager !== undefined && requestedManager !== \"npm\" && requestedManager !== \"pnpm\") {\n throw new Error(\"--package-manager must be npm or pnpm\");\n }\n const packageManager = (requestedManager as PackageManager | undefined) ?? detectPackageManager() ?? \"pnpm\";\n const result = scaffoldGame({\n directory: positionals[0],\n org: values.org,\n packageManager,\n });\n console.log(`Created ${result.name} in ${result.root}`);\n}\n\ntry {\n main(process.argv.slice(2));\n} catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`create-eigen-game: ${message}`);\n process.exitCode = 1;\n}\n"],"mappings":";;;;;;;AACA,SAAS,iBAAiB;AAG1B,IAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWb,SAAS,KAAK,MAAsB;AAClC,QAAM,EAAE,QAAQ,YAAY,IAAI,UAAU;AAAA,IACxC;AAAA,IACA,kBAAkB;AAAA,IAClB,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,MAAM,EAAE,MAAM,WAAW,OAAO,IAAI;AAAA,MACpC,KAAK,EAAE,MAAM,SAAS;AAAA,MACtB,mBAAmB,EAAE,MAAM,SAAS;AAAA,IACtC;AAAA,EACF,CAAC;AAED,MAAI,OAAO,MAAM;AACf,YAAQ,IAAI,IAAI;AAChB;AAAA,EACF;AACA,MAAI,YAAY,WAAW,GAAG;AAC5B,UAAM,IAAI,MAAM;AAAA;AAAA,EAAiD,IAAI,EAAE;AAAA,EACzE;AAEA,QAAM,mBAAmB,OAAO,iBAAiB;AACjD,MAAI,qBAAqB,UAAa,qBAAqB,SAAS,qBAAqB,QAAQ;AAC/F,UAAM,IAAI,MAAM,uCAAuC;AAAA,EACzD;AACA,QAAM,iBAAkB,oBAAmD,qBAAqB,KAAK;AACrG,QAAM,SAAS,aAAa;AAAA,IAC1B,WAAW,YAAY,CAAC;AAAA,IACxB,KAAK,OAAO;AAAA,IACZ;AAAA,EACF,CAAC;AACD,UAAQ,IAAI,WAAW,OAAO,IAAI,OAAO,OAAO,IAAI,EAAE;AACxD;AAEA,IAAI;AACF,OAAK,QAAQ,KAAK,MAAM,CAAC,CAAC;AAC5B,SAAS,OAAO;AACd,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,UAAQ,MAAM,sBAAsB,OAAO,EAAE;AAC7C,UAAQ,WAAW;AACrB;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type PackageManager = "npm" | "pnpm";
|
|
2
|
+
interface ScaffoldOptions {
|
|
3
|
+
directory: string;
|
|
4
|
+
org?: string;
|
|
5
|
+
packageManager?: PackageManager;
|
|
6
|
+
/**
|
|
7
|
+
* Run Flutter, package installation, and contract/payload generation.
|
|
8
|
+
* Kept as a programmatic test seam; the public CLI always bootstraps both
|
|
9
|
+
* halves of the combined project.
|
|
10
|
+
*/
|
|
11
|
+
bootstrap?: boolean;
|
|
12
|
+
run?: (command: string, args: string[], cwd: string) => void;
|
|
13
|
+
}
|
|
14
|
+
interface ScaffoldResult {
|
|
15
|
+
root: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}
|
|
18
|
+
declare function detectPackageManager(userAgent?: string | undefined): PackageManager | undefined;
|
|
19
|
+
declare function scaffoldGame(options: ScaffoldOptions): ScaffoldResult;
|
|
20
|
+
|
|
21
|
+
export { type PackageManager, type ScaffoldOptions, type ScaffoldResult, detectPackageManager, scaffoldGame };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-eigen-game",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Scaffold an Eigen game Worker and Flutter app without cloning the engine repositories.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-eigen-game": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"templates"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^26.1.1",
|
|
23
|
+
"tsup": "^8.5.1",
|
|
24
|
+
"typescript": "^5.9.3",
|
|
25
|
+
"vitest": "^4.1.10",
|
|
26
|
+
"wrangler": "^4.110.0",
|
|
27
|
+
"zod": "^4.4.3",
|
|
28
|
+
"@eigeninteractive/rules": "0.1.0",
|
|
29
|
+
"@eigeninteractive/testkit": "0.1.0",
|
|
30
|
+
"@eigeninteractive/server": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/eigeninteractive/eigen-server.git",
|
|
41
|
+
"directory": "packages/create-eigen-game"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup src/index.ts src/cli.ts --format esm --dts --sourcemap --clean",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p templates/worker/tsconfig.json"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import 'package:firebase_core/firebase_core.dart';
|
|
2
|
+
|
|
3
|
+
/// Firebase configuration generated by the FlutterFire CLI.
|
|
4
|
+
///
|
|
5
|
+
/// The scaffold keeps this throwing implementation so the project compiles
|
|
6
|
+
/// before Firebase is connected. `pnpm firebase:configure` or
|
|
7
|
+
/// `npm run firebase:configure` replaces this file with FlutterFire's generated
|
|
8
|
+
/// platform configuration.
|
|
9
|
+
abstract final class DefaultFirebaseOptions {
|
|
10
|
+
static FirebaseOptions get currentPlatform => throw StateError(
|
|
11
|
+
'Firebase is not configured. Run `pnpm firebase:configure` or '
|
|
12
|
+
'`npm run firebase:configure` from the project root.',
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Example Game client module
|
|
2
|
+
|
|
3
|
+
The immutable payload classes and typed rules base under `generated/` come from
|
|
4
|
+
the authoritative TypeScript schemas. Regenerate them after changing a schema
|
|
5
|
+
or shared fixture:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
dart run eigen_flutter:generate_payloads \
|
|
9
|
+
--contract ../server/game-contract.json \
|
|
10
|
+
--output lib/game/generated/payloads.dart \
|
|
11
|
+
--fixtures-output test/fixtures
|
|
12
|
+
flutter test
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Keep `module.dart` and `v1/rules.dart` handwritten: they contain client-side
|
|
16
|
+
legality and preview behavior plus game presentation. The server remains
|
|
17
|
+
authoritative. The generated `test/game/twin_fixtures_test.dart` runs the
|
|
18
|
+
contract's same fixtures against this Dart rules registry.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import 'package:eigen_flutter/eigen_flutter.dart';
|
|
2
|
+
import 'package:flutter/material.dart';
|
|
3
|
+
|
|
4
|
+
import 'v1/rules.dart';
|
|
5
|
+
|
|
6
|
+
class ExampleGameModule extends GameModule {
|
|
7
|
+
const ExampleGameModule();
|
|
8
|
+
|
|
9
|
+
@override
|
|
10
|
+
Map<int, GameRules> get versions => const {1: ExampleGameRulesV1()};
|
|
11
|
+
|
|
12
|
+
@override
|
|
13
|
+
GameCreationSpec get creationSpec => const GameCreationSpec(
|
|
14
|
+
minPlayers: 2,
|
|
15
|
+
maxPlayers: 2,
|
|
16
|
+
timingConfigs: {'Untimed': UntimedConfig()},
|
|
17
|
+
defaultConfig: {'target': 10},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
Widget? buildCreationConfig({
|
|
22
|
+
required ValueChanged<Map<String, dynamic>> onChanged,
|
|
23
|
+
}) => null;
|
|
24
|
+
|
|
25
|
+
@override
|
|
26
|
+
Widget buildRules(BuildContext context) => const Text(
|
|
27
|
+
'Take turns increasing the counter. The first player to reach the target wins.',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import 'package:eigen_flutter/eigen_flutter.dart';
|
|
2
|
+
import 'package:flutter/material.dart';
|
|
3
|
+
|
|
4
|
+
import '../generated/payloads.dart';
|
|
5
|
+
|
|
6
|
+
class ExampleGameRulesV1 extends ExampleGameV1RulesBase {
|
|
7
|
+
const ExampleGameRulesV1();
|
|
8
|
+
|
|
9
|
+
@override
|
|
10
|
+
bool isValidAction({
|
|
11
|
+
required ExampleGameV1Observation obs,
|
|
12
|
+
required List<int> pending,
|
|
13
|
+
required ExampleGameV1Action data,
|
|
14
|
+
required int playerIndex,
|
|
15
|
+
required ExampleGameV1Config config,
|
|
16
|
+
}) => pending.contains(playerIndex) && data.amount > 0;
|
|
17
|
+
|
|
18
|
+
@override
|
|
19
|
+
ExampleGameV1Observation? previewAction({
|
|
20
|
+
required ExampleGameV1Observation obs,
|
|
21
|
+
required List<int> pending,
|
|
22
|
+
required ExampleGameV1Action data,
|
|
23
|
+
required int playerIndex,
|
|
24
|
+
required ExampleGameV1Config config,
|
|
25
|
+
}) => ExampleGameV1Observation(count: obs.count + data.amount);
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
Widget buildContent(GameContentContext context) =>
|
|
29
|
+
_CounterBoard(context: context, rules: this);
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
String? ratingPool(RatingPoolArgs args) => null;
|
|
33
|
+
|
|
34
|
+
@override
|
|
35
|
+
bool botSeatable(BotSeatableArgs args) => true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class _CounterBoard extends StatelessWidget {
|
|
39
|
+
const _CounterBoard({required this.context, required this.rules});
|
|
40
|
+
|
|
41
|
+
final GameContentContext context;
|
|
42
|
+
final ExampleGameRulesV1 rules;
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
Widget build(BuildContext buildContext) {
|
|
46
|
+
final observation = context.frame.observation! as ExampleGameV1Observation;
|
|
47
|
+
final config = context.config as ExampleGameV1Config;
|
|
48
|
+
final seat = context.mySeat.indexOrNull;
|
|
49
|
+
final action = ExampleGameV1Action(amount: 1);
|
|
50
|
+
final canAct =
|
|
51
|
+
seat != null &&
|
|
52
|
+
!context.isReplay &&
|
|
53
|
+
!context.actionPending &&
|
|
54
|
+
rules.isValidAction(
|
|
55
|
+
obs: observation,
|
|
56
|
+
pending: context.frame.pendingPlayers,
|
|
57
|
+
data: action,
|
|
58
|
+
playerIndex: seat,
|
|
59
|
+
config: config,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return Center(
|
|
63
|
+
child: Column(
|
|
64
|
+
mainAxisSize: MainAxisSize.min,
|
|
65
|
+
children: [
|
|
66
|
+
Text(
|
|
67
|
+
'${observation.count} / ${config.target}',
|
|
68
|
+
style: Theme.of(buildContext).textTheme.displayMedium,
|
|
69
|
+
),
|
|
70
|
+
const SizedBox(height: 24),
|
|
71
|
+
FilledButton(
|
|
72
|
+
onPressed: canAct ? () => context.onAction(action.toJson()) : null,
|
|
73
|
+
child: const Text('Add one'),
|
|
74
|
+
),
|
|
75
|
+
],
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import 'package:eigen_flutter/eigen_flutter.dart';
|
|
2
|
+
import 'package:firebase_core/firebase_core.dart';
|
|
3
|
+
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
4
|
+
import 'package:flutter/material.dart';
|
|
5
|
+
|
|
6
|
+
import 'firebase_options.dart';
|
|
7
|
+
import 'game/module.dart';
|
|
8
|
+
|
|
9
|
+
const _apiBaseUrl = String.fromEnvironment('API_BASE_URL');
|
|
10
|
+
const _googleWebClientId = String.fromEnvironment('GOOGLE_WEB_CLIENT_ID');
|
|
11
|
+
const _firebaseVapidKey = String.fromEnvironment('FIREBASE_VAPID_KEY');
|
|
12
|
+
const _appHost = String.fromEnvironment('APP_HOST');
|
|
13
|
+
|
|
14
|
+
Future<void> main() async {
|
|
15
|
+
await runEngineApp(
|
|
16
|
+
module: const ExampleGameModule(),
|
|
17
|
+
config: AppConfig(
|
|
18
|
+
branding: const Branding(
|
|
19
|
+
appName: 'Example Game',
|
|
20
|
+
seedColor: Colors.indigo,
|
|
21
|
+
),
|
|
22
|
+
engine: EngineConfig(
|
|
23
|
+
apiBaseUrl: _apiBaseUrl,
|
|
24
|
+
googleWebClientId: _googleWebClientId,
|
|
25
|
+
appHost: _appHost.isEmpty ? null : _appHost,
|
|
26
|
+
firebaseVapidKey: _firebaseVapidKey,
|
|
27
|
+
),
|
|
28
|
+
),
|
|
29
|
+
firebaseOptions: DefaultFirebaseOptions.currentPlatform,
|
|
30
|
+
onBackgroundMessage: _onBackgroundMessage,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@pragma('vm:entry-point')
|
|
35
|
+
Future<void> _onBackgroundMessage(RemoteMessage message) async {
|
|
36
|
+
await Firebase.initializeApp(
|
|
37
|
+
options: DefaultFirebaseOptions.currentPlatform,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import 'package:eigen_flutter/testing/twin_fixtures.dart';
|
|
2
|
+
import 'package:example_game/game/module.dart';
|
|
3
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
4
|
+
|
|
5
|
+
void main() {
|
|
6
|
+
const module = ExampleGameModule();
|
|
7
|
+
final suites = loadTwinFixtureSuites('test/fixtures');
|
|
8
|
+
|
|
9
|
+
test('the generated twin fixture suite is present', () {
|
|
10
|
+
expect(suites, isNotEmpty);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
for (final suite in suites) {
|
|
14
|
+
final rules = module.versions[suite.schemaVersion];
|
|
15
|
+
group('twin fixtures v${suite.schemaVersion} (${suite.path})', () {
|
|
16
|
+
for (final fixtureCase in suite.cases) {
|
|
17
|
+
test(fixtureCase.name, () {
|
|
18
|
+
expect(
|
|
19
|
+
rules,
|
|
20
|
+
isNotNull,
|
|
21
|
+
reason: 'no Dart rules unit for v${suite.schemaVersion}',
|
|
22
|
+
);
|
|
23
|
+
expect(runTwinFixtureCase(rules!, fixtureCase), isEmpty);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Keep this version aligned with the Firebase JS SDK supported by the
|
|
2
|
+
// firebase_core_web version resolved by the Flutter app.
|
|
3
|
+
importScripts("https://www.gstatic.com/firebasejs/12.15.0/firebase-app-compat.js");
|
|
4
|
+
importScripts("https://www.gstatic.com/firebasejs/12.15.0/firebase-messaging-compat.js");
|
|
5
|
+
importScripts("firebase-config.js");
|
|
6
|
+
|
|
7
|
+
// `firebase:configure` generates this public configuration from the Web app
|
|
8
|
+
// selected by FlutterFire. Firebase Security Rules and the Eigen Worker's
|
|
9
|
+
// verified ID tokens enforce authorization; these identifiers are not server
|
|
10
|
+
// credentials.
|
|
11
|
+
firebase.initializeApp(self.firebaseConfig);
|
|
12
|
+
|
|
13
|
+
// Initializing Messaging is enough for notification payloads: the browser
|
|
14
|
+
// displays them while the app is backgrounded or closed.
|
|
15
|
+
firebase.messaging();
|