create-syncular-app 0.1.3 → 0.2.1
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 +70 -27
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +131 -179
- package/dist/constants.d.ts +40 -0
- package/dist/constants.js +46 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/scaffold.d.ts +44 -0
- package/dist/scaffold.js +106 -0
- package/package.json +22 -15
- package/src/cli.ts +174 -0
- package/src/constants.ts +53 -0
- package/src/index.ts +3 -0
- package/src/scaffold.ts +161 -0
- package/template/minimal/README.md +45 -0
- package/template/minimal/gitignore +5 -0
- package/template/minimal/migrations/0001_initial/up.sql +9 -0
- package/template/minimal/package.json +22 -0
- package/template/minimal/src/clients.ts +54 -0
- package/template/minimal/src/make-client.ts +26 -0
- package/template/minimal/src/server.ts +39 -0
- package/template/minimal/src/smoke.test.ts +70 -0
- package/template/minimal/src/syncular.generated.ts +66 -0
- package/template/minimal/syncular.ir.json +66 -0
- package/template/minimal/syncular.json +17 -0
- package/template/minimal/tsconfig.json +17 -0
- package/template/web/README.md +63 -0
- package/template/web/gitignore +5 -0
- package/template/web/migrations/0001_initial/up.sql +11 -0
- package/template/web/package.json +22 -0
- package/template/web/src/frontend/index.html +37 -0
- package/template/web/src/frontend/main.ts +191 -0
- package/template/web/src/frontend/worker.ts +9 -0
- package/template/web/src/server.ts +183 -0
- package/template/web/src/smoke.test.ts +91 -0
- package/template/web/src/syncular.generated.ts +74 -0
- package/template/web/syncular.ir.json +76 -0
- package/template/web/syncular.json +17 -0
- package/template/web/tsconfig.json +17 -0
- package/template/README.md +0 -122
- package/template/_gitignore +0 -5
- package/template/generated/kotlin/SyncularApp.kt +0 -1005
- package/template/generated/rust/diesel_tables.rs +0 -142
- package/template/generated/rust/migrations.rs +0 -32
- package/template/generated/rust/schema.rs +0 -15
- package/template/generated/rust/syncular.rs +0 -926
- package/template/generated/swift/SyncularApp.swift +0 -1191
- package/template/generated/syncular.codegen.json +0 -19
- package/template/index.html +0 -13
- package/template/migrations/0001_initial/down.sql +0 -1
- package/template/migrations/0001_initial/up.sql +0 -8
- package/template/package.json +0 -33
- package/template/scripts/dev.ts +0 -42
- package/template/src/app.tsx +0 -231
- package/template/src/client/syncular.ts +0 -61
- package/template/src/generated/syncular.generated.ts +0 -769
- package/template/src/generated/syncular.server.generated.ts +0 -512
- package/template/src/main.tsx +0 -5
- package/template/src/server/sync-server.ts +0 -129
- package/template/src/styles.css +0 -233
- package/template/syncular.app.ts +0 -23
- package/template/syncular.schema.json +0 -145
- package/template/tsconfig.json +0 -18
- package/template/vite.config.ts +0 -9
package/README.md
CHANGED
|
@@ -1,39 +1,82 @@
|
|
|
1
1
|
# create-syncular-app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The `create-syncular-app` scaffolder — the create-app experience for syncular v2.
|
|
4
4
|
|
|
5
|
-
```
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
npm create syncular-app@latest my-app
|
|
9
|
-
pnpm create syncular-app my-app
|
|
5
|
+
```sh
|
|
6
|
+
bun create syncular-app my-app # prompts for the template
|
|
7
|
+
bunx create-syncular-app my-app --template web
|
|
10
8
|
```
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
> Naming (`@syncular/*`, bin `create-syncular-app`, CLI `syncular`) is not
|
|
11
|
+
> final — package identity is TODO 6.3. Every user-visible name lives in
|
|
12
|
+
> [`src/constants.ts`](./src/constants.ts) so a rename is mechanical: edit the
|
|
13
|
+
> constants, regenerate the templates' generated files, done.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
## Templates
|
|
16
|
+
|
|
17
|
+
| Template | Shape |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `minimal` | Server + a terminal two-client convergence demo (no browser) — migrations + manifest + `generate` wiring. Copy-evolved from `examples/quickstart`. The smallest honest starting point. |
|
|
20
|
+
| `web` | Hono server + WebSocket realtime + a single-pane browser todo app whose whole client core runs in a Web Worker on OPFS. Derived from `apps/demo`, slimmed to one pane (no conflict simulator, no blob attachments) — the minimal browser app a real user starts from. |
|
|
21
|
+
|
|
22
|
+
> **Third-template candidate: `react`.** [`apps/demo-react`](../../apps/demo-react)
|
|
23
|
+
> is the ready-made source for a hooks-based template — `@syncular/react`
|
|
24
|
+
> (`SyncProvider` + `useTypedQuery` + `useMutation` + `useSyncStatus` +
|
|
25
|
+
> `useWindow`) over the same worker + OPFS core, with the Kysely-typed read
|
|
26
|
+
> layer wired. Slim it the way `web` slims `apps/demo` (drop the three-list
|
|
27
|
+
> seed to one, keep one hook of each kind) and add it here when a React
|
|
28
|
+
> template is wanted. Not built yet — noted so the shape is on record.
|
|
29
|
+
|
|
30
|
+
Each template ships its own `README.md` (run steps, what to edit first),
|
|
31
|
+
`.gitignore` (as `gitignore` — see below), a working `tsconfig.json`, and a
|
|
32
|
+
smoke test.
|
|
33
|
+
|
|
34
|
+
## The local-vs-published dependency mechanism
|
|
35
|
+
|
|
36
|
+
Template `package.json` files use `workspace:*` ranges for every
|
|
37
|
+
`@syncular/*` dependency. At scaffold time the scaffolder rewrites those
|
|
38
|
+
ranges:
|
|
39
|
+
|
|
40
|
+
- **`--local`** (or the in-tree test path): keep `workspace:*` verbatim. These
|
|
41
|
+
are the only ranges that resolve when the scaffolded app sits inside this
|
|
42
|
+
repo's workspace.
|
|
43
|
+
- **default** (a normal `bunx create-…` run): rewrite to
|
|
44
|
+
`PUBLISHED_DEPENDENCY_RANGE` (`src/constants.ts`). **Today that constant is
|
|
45
|
+
*also* `workspace:*`** because the v2 packages are unpublished and
|
|
46
|
+
version-less (all `private`, no `version` — TODO 6.3), so there is no honest
|
|
47
|
+
semver range yet. The CLI **warns loudly** in this case: a `bun install`
|
|
48
|
+
outside the repo cannot resolve the deps until publishing lands. When the
|
|
49
|
+
packages ship, flip that one constant to `^<version>` (or teach the CLI to
|
|
50
|
+
read the published version) — a single edit.
|
|
51
|
+
|
|
52
|
+
`.gitignore` ships as `gitignore` (no dot) because npm strips real dotfiles
|
|
53
|
+
from published tarballs; the scaffolder renames it on copy.
|
|
19
54
|
|
|
20
|
-
|
|
55
|
+
Placeholder substitution is deliberately dumb and greppable: the only token is
|
|
56
|
+
`__PROJECT_NAME__` (in each template's `package.json` and `README.md`),
|
|
57
|
+
replaced with the derived package name.
|
|
21
58
|
|
|
22
|
-
|
|
23
|
-
`syncular.app.ts`
|
|
24
|
-
- a committed, pre-generated TypeScript client (`src/generated/`) — no extra
|
|
25
|
-
toolchain needed to run; regenerate with `npx syncular generate`
|
|
26
|
-
- a Hono sync server on Bun + SQLite (`src/server/sync-server.ts`)
|
|
27
|
-
- a React UI on `@syncular/client/react` with live queries and offline-queued
|
|
28
|
-
mutations (`src/app.tsx`)
|
|
59
|
+
## How the templates are tested (and how the tier splits)
|
|
29
60
|
|
|
30
|
-
|
|
31
|
-
|
|
61
|
+
`test/scaffold.test.ts` exercises the TEMPLATES THEMSELVES, not just the
|
|
62
|
+
scaffolder logic. For each template it:
|
|
32
63
|
|
|
33
|
-
|
|
64
|
+
1. scaffolds into a temp dir (`--local`),
|
|
65
|
+
2. asserts the tree shape + placeholder substitution + package.json rewrite,
|
|
66
|
+
3. runs `syncular generate --check` — proving the committed
|
|
67
|
+
`syncular.generated.ts` is byte-fresh,
|
|
68
|
+
4. links a `node_modules` into the temp dir **offline** (see
|
|
69
|
+
[`test/link-workspace.ts`](./test/link-workspace.ts): `@syncular/*` →
|
|
70
|
+
the real package dirs, external deps → the workspace `.bun` hoist store),
|
|
71
|
+
typechecks the template's own files, and runs the app's own `bun test`
|
|
72
|
+
smoke.
|
|
34
73
|
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
|
|
74
|
+
Steps 1–4 are the **always-run tier** — offline and fast (~1.5s total), so
|
|
75
|
+
they ride the normal `bun run check`. A **full-fidelity tier** behind
|
|
76
|
+
`SYNCULAR_TEMPLATE_INSTALL=1` additionally does a real `bun install` per
|
|
77
|
+
template before running the smoke; it is opt-in because it needs the network.
|
|
38
78
|
|
|
39
|
-
|
|
79
|
+
The in-tree template `*.test.ts` files are excluded from the root `bun test`
|
|
80
|
+
sweep (`--path-ignore-patterns '**/create-app/template/**'` in the root `test`
|
|
81
|
+
script) — they can only resolve their deps inside a scaffolded, linked copy,
|
|
82
|
+
which the tier test above provides.
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { type TemplateName } from './scaffold.js';
|
|
3
|
+
interface ParsedArgs {
|
|
4
|
+
readonly help: boolean;
|
|
5
|
+
readonly local: boolean;
|
|
6
|
+
readonly projectName?: string;
|
|
7
|
+
readonly template?: TemplateName;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
10
|
+
export declare function runCli(argv?: string[]): Promise<number>;
|
|
11
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,197 +1,149 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from
|
|
12
|
-
import {
|
|
13
|
-
import { createInterface } from "node:readline/promises";
|
|
14
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
|
-
var FALLBACK_SYNCULAR_VERSION_RANGE = "^0.1.3";
|
|
2
|
+
/**
|
|
3
|
+
* `create-syncular-app [project-name] [--template <minimal|web>] [--local]`
|
|
4
|
+
*
|
|
5
|
+
* Runnable as `bun create syncular-app my-app` or `bunx create-syncular-app
|
|
6
|
+
* my-app`. Prompts for anything a flag did not supply. Scaffolds one of the
|
|
7
|
+
* two templates (see `./scaffold`).
|
|
8
|
+
*/
|
|
9
|
+
import { relative } from 'node:path';
|
|
10
|
+
import { createInterface } from 'node:readline/promises';
|
|
11
|
+
import { CLI_BIN, CREATE_BIN, PRODUCT_NAME } from './constants.js';
|
|
12
|
+
import { isTemplateName, scaffoldApp, TEMPLATES, } from './scaffold.js';
|
|
16
13
|
function usage() {
|
|
17
|
-
|
|
14
|
+
return `usage: ${CREATE_BIN} [project-name] [options]
|
|
15
|
+
|
|
16
|
+
Scaffolds a ${PRODUCT_NAME} v2 starter app.
|
|
17
|
+
|
|
18
|
+
templates:
|
|
19
|
+
minimal server + a terminal two-client convergence demo (no browser)
|
|
20
|
+
web Hono server + WebSocket + a single-pane browser todo app
|
|
21
|
+
(worker core on OPFS)
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
options:
|
|
24
|
+
--template <name> one of: ${TEMPLATES.join(', ')} (prompts if omitted)
|
|
25
|
+
--local keep workspace:* deps (scaffolding inside this repo)
|
|
26
|
+
-h, --help show this help
|
|
21
27
|
|
|
22
28
|
examples:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
pnpm create syncular-app my-app
|
|
29
|
+
bun create syncular-app my-app
|
|
30
|
+
bunx ${CREATE_BIN} my-app --template web
|
|
26
31
|
`;
|
|
27
32
|
}
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
export function parseArgs(argv) {
|
|
34
|
+
let help = false;
|
|
35
|
+
let local = false;
|
|
36
|
+
let projectName;
|
|
37
|
+
let template;
|
|
38
|
+
for (let i = 0; i < argv.length; i++) {
|
|
39
|
+
const arg = argv[i];
|
|
40
|
+
if (arg === '-h' || arg === '--help') {
|
|
41
|
+
help = true;
|
|
42
|
+
}
|
|
43
|
+
else if (arg === '--local') {
|
|
44
|
+
local = true;
|
|
45
|
+
}
|
|
46
|
+
else if (arg === '--template') {
|
|
47
|
+
const value = argv[i + 1];
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
throw new Error('--template requires a value');
|
|
50
|
+
if (!isTemplateName(value)) {
|
|
51
|
+
throw new Error(`unknown template ${JSON.stringify(value)} (expected: ${TEMPLATES.join(', ')})`);
|
|
52
|
+
}
|
|
53
|
+
template = value;
|
|
54
|
+
i += 1;
|
|
55
|
+
}
|
|
56
|
+
else if (arg?.startsWith('-')) {
|
|
57
|
+
throw new Error(`unknown option ${JSON.stringify(arg)}\n\n${usage()}`);
|
|
58
|
+
}
|
|
59
|
+
else if (projectName === undefined) {
|
|
60
|
+
projectName = arg;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
throw new Error(`unexpected extra argument ${JSON.stringify(arg)}`);
|
|
64
|
+
}
|
|
34
65
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
66
|
+
return {
|
|
67
|
+
help,
|
|
68
|
+
local,
|
|
69
|
+
...(projectName !== undefined ? { projectName } : {}),
|
|
70
|
+
...(template !== undefined ? { template } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function prompt(question) {
|
|
74
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
75
|
+
try {
|
|
76
|
+
return (await rl.question(question)).trim();
|
|
39
77
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
${usage()}`);
|
|
78
|
+
finally {
|
|
79
|
+
rl.close();
|
|
44
80
|
}
|
|
45
|
-
options.targetDir = arg;
|
|
46
|
-
}
|
|
47
|
-
return options;
|
|
48
|
-
}
|
|
49
|
-
function packageNameFromDirectory(targetDir) {
|
|
50
|
-
const name = basename(resolve(targetDir)).toLowerCase().replace(/[^a-z0-9-_.~]+/g, "-").replace(/^[-._]+|[-._]+$/g, "");
|
|
51
|
-
return name.length > 0 ? name : "syncular-app";
|
|
52
81
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return dependencyName === "syncular" || dependencyName.startsWith("@syncular/");
|
|
62
|
-
}
|
|
63
|
-
function rewriteTemplatePackageJson(source, options) {
|
|
64
|
-
const pkg = JSON.parse(source);
|
|
65
|
-
pkg.name = options.packageName;
|
|
66
|
-
for (const section of [pkg.dependencies, pkg.devDependencies]) {
|
|
67
|
-
if (!section)
|
|
68
|
-
continue;
|
|
69
|
-
for (const [name, range] of Object.entries(section)) {
|
|
70
|
-
if (isSyncularPackage(name) && range.startsWith("workspace:")) {
|
|
71
|
-
section[name] = options.syncularRange;
|
|
72
|
-
}
|
|
82
|
+
async function resolveTemplate(supplied) {
|
|
83
|
+
if (supplied !== undefined)
|
|
84
|
+
return supplied;
|
|
85
|
+
const answer = await prompt(`Template? (${TEMPLATES.join(' / ')}) [minimal]: `);
|
|
86
|
+
if (answer === '')
|
|
87
|
+
return 'minimal';
|
|
88
|
+
if (!isTemplateName(answer)) {
|
|
89
|
+
throw new Error(`unknown template ${JSON.stringify(answer)} (expected: ${TEMPLATES.join(', ')})`);
|
|
73
90
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
Done. Next steps:
|
|
96
|
-
|
|
97
|
-
cd ${targetDir}
|
|
98
|
-
${install}
|
|
99
|
-
${dev}
|
|
100
|
-
|
|
101
|
-
The dev script starts the sync server (http://127.0.0.1:4100/health) and the
|
|
102
|
-
Vite client (http://127.0.0.1:5173). The app itself runs on Bun (the sync
|
|
103
|
-
server uses Bun.serve + bun:sqlite); install it from https://bun.sh if needed.
|
|
104
|
-
|
|
105
|
-
Read the generated README.md for the project layout and how to evolve the
|
|
106
|
-
schema.
|
|
107
|
-
`;
|
|
108
|
-
}
|
|
109
|
-
function readOwnVersion() {
|
|
110
|
-
try {
|
|
111
|
-
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
112
|
-
return packageJson.version;
|
|
113
|
-
} catch {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function templateDirectory() {
|
|
118
|
-
return fileURLToPath(new URL("../template", import.meta.url));
|
|
119
|
-
}
|
|
120
|
-
function directoryIsEmpty(path) {
|
|
121
|
-
return readdirSync(path).length === 0;
|
|
122
|
-
}
|
|
123
|
-
async function promptTargetDir() {
|
|
124
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
125
|
-
try {
|
|
126
|
-
const answer = await rl.question("Target directory (e.g. my-app): ");
|
|
127
|
-
return answer.trim();
|
|
128
|
-
} finally {
|
|
129
|
-
rl.close();
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
function scaffoldApp(targetDirInput) {
|
|
133
|
-
const targetDir = resolve(targetDirInput);
|
|
134
|
-
const templateDir = templateDirectory();
|
|
135
|
-
if (!existsSync(templateDir)) {
|
|
136
|
-
throw new Error(`Bundled template not found at ${templateDir}`);
|
|
137
|
-
}
|
|
138
|
-
if (existsSync(targetDir) && !directoryIsEmpty(targetDir)) {
|
|
139
|
-
throw new Error(`Target directory is not empty: ${targetDir}`);
|
|
140
|
-
}
|
|
141
|
-
cpSync(templateDir, targetDir, { recursive: true });
|
|
142
|
-
const gitignorePlaceholder = join(targetDir, "_gitignore");
|
|
143
|
-
if (existsSync(gitignorePlaceholder)) {
|
|
144
|
-
renameSync(gitignorePlaceholder, join(targetDir, ".gitignore"));
|
|
145
|
-
}
|
|
146
|
-
const packageName = packageNameFromDirectory(targetDir);
|
|
147
|
-
const syncularRange = syncularDependencyRange(readOwnVersion());
|
|
148
|
-
const packageJsonPath = join(targetDir, "package.json");
|
|
149
|
-
writeFileSync(packageJsonPath, rewriteTemplatePackageJson(readFileSync(packageJsonPath, "utf8"), {
|
|
150
|
-
packageName,
|
|
151
|
-
syncularRange
|
|
152
|
-
}));
|
|
153
|
-
return { targetDir, packageName, syncularRange };
|
|
154
|
-
}
|
|
155
|
-
async function runCreateSyncularAppCli(argv = process.argv.slice(2)) {
|
|
156
|
-
try {
|
|
157
|
-
const options = parseCreateCliArgs(argv);
|
|
158
|
-
if (options.help) {
|
|
159
|
-
console.log(usage());
|
|
160
|
-
return 0;
|
|
91
|
+
return answer;
|
|
92
|
+
}
|
|
93
|
+
function nextSteps(dir, template) {
|
|
94
|
+
const cd = ` cd ${dir}\n bun install`;
|
|
95
|
+
const run = template === 'web'
|
|
96
|
+
? ' bun run dev # http://localhost:8787'
|
|
97
|
+
: ' bun run server # terminal 1\n bun run clients # terminal 2 — prints "✓ converged"';
|
|
98
|
+
return `\nDone. Next steps:\n\n${cd}\n bun run generate # migrations → src/syncular.generated.ts\n${run}\n\nSee README.md for the layout and what to edit first.\n`;
|
|
99
|
+
}
|
|
100
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
101
|
+
let args;
|
|
102
|
+
try {
|
|
103
|
+
args = parseArgs(argv);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.error(`[${CREATE_BIN}] ${describe(error)}`);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
if (args.help) {
|
|
110
|
+
console.log(usage());
|
|
111
|
+
return 0;
|
|
161
112
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
113
|
+
try {
|
|
114
|
+
let projectName = args.projectName;
|
|
115
|
+
if (projectName === undefined || projectName === '') {
|
|
116
|
+
projectName = await prompt('Project directory (e.g. my-app): ');
|
|
117
|
+
}
|
|
118
|
+
if (projectName === '') {
|
|
119
|
+
console.error(`[${CREATE_BIN}] a project directory is required`);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
const template = await resolveTemplate(args.template);
|
|
123
|
+
const result = scaffoldApp({
|
|
124
|
+
template,
|
|
125
|
+
targetDir: projectName,
|
|
126
|
+
local: args.local,
|
|
127
|
+
});
|
|
128
|
+
const displayDir = relative(process.cwd(), result.targetDir) || '.';
|
|
129
|
+
console.log(`[${CREATE_BIN}] scaffolded ${result.packageName} (template: ${template}) into ${displayDir}`);
|
|
130
|
+
if (!result.local) {
|
|
131
|
+
console.warn(`[${CREATE_BIN}] note: the ${PRODUCT_NAME} v2 packages are not published yet, so\n` +
|
|
132
|
+
` the app's dependencies are still workspace:* ranges. Run inside this repo\n` +
|
|
133
|
+
` (or pass --local) until publishing lands (TODO 6.3). Use \`${CLI_BIN} generate\`\n` +
|
|
134
|
+
' to (re)generate the typed schema.');
|
|
135
|
+
}
|
|
136
|
+
console.log(nextSteps(displayDir, template));
|
|
137
|
+
return 0;
|
|
165
138
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return 1;
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.error(`[${CREATE_BIN}] ${describe(error)}`);
|
|
141
|
+
return 1;
|
|
170
142
|
}
|
|
171
|
-
const result = scaffoldApp(targetDirInput);
|
|
172
|
-
console.log(`[create-syncular-app] Scaffolded ${result.packageName} into ${result.targetDir} (Syncular packages at ${result.syncularRange}).`);
|
|
173
|
-
console.log(nextStepsMessage(targetDirInput, detectPackageManager()));
|
|
174
|
-
return 0;
|
|
175
|
-
} catch (error) {
|
|
176
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
-
console.error(`[create-syncular-app] ${message}`);
|
|
178
|
-
return 1;
|
|
179
|
-
}
|
|
180
143
|
}
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
return entrypoint !== undefined && import.meta.url === pathToFileURL(entrypoint).href;
|
|
144
|
+
function describe(error) {
|
|
145
|
+
return error instanceof Error ? error.message : String(error);
|
|
184
146
|
}
|
|
185
|
-
if (
|
|
186
|
-
|
|
147
|
+
if (import.meta.main) {
|
|
148
|
+
process.exitCode = await runCli();
|
|
187
149
|
}
|
|
188
|
-
export {
|
|
189
|
-
syncularDependencyRange,
|
|
190
|
-
scaffoldApp,
|
|
191
|
-
runCreateSyncularAppCli,
|
|
192
|
-
rewriteTemplatePackageJson,
|
|
193
|
-
parseCreateCliArgs,
|
|
194
|
-
packageNameFromDirectory,
|
|
195
|
-
nextStepsMessage,
|
|
196
|
-
detectPackageManager
|
|
197
|
-
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE place naming lives (TODO 6.3 is a product decision — final package
|
|
3
|
+
* identity is not settled). Every user-visible name the scaffolder emits comes
|
|
4
|
+
* from here so a rename is mechanical: change these constants, regenerate the
|
|
5
|
+
* templates' generated files, done. No name is hardcoded elsewhere in this
|
|
6
|
+
* package — grep for the literals below and you should only find them here.
|
|
7
|
+
*/
|
|
8
|
+
/** The scoped npm namespace for the v2 packages, e.g. `@syncular`. */
|
|
9
|
+
export declare const PACKAGE_SCOPE = "@syncular";
|
|
10
|
+
/** The typegen CLI binary name (`syncular generate`, `syncular init`). */
|
|
11
|
+
export declare const CLI_BIN = "syncular";
|
|
12
|
+
/** This package's own bin (`bun create syncular …` / `bunx create-syncular-app …`). */
|
|
13
|
+
export declare const CREATE_BIN = "create-syncular-app";
|
|
14
|
+
/** Human-facing product name in prose. */
|
|
15
|
+
export declare const PRODUCT_NAME = "syncular";
|
|
16
|
+
/**
|
|
17
|
+
* The scoped workspace packages a template may depend on. The scaffolder only
|
|
18
|
+
* rewrites ranges for packages under {@link PACKAGE_SCOPE}, so this list is
|
|
19
|
+
* documentation, not a gate — but it keeps the "what does a template pull in"
|
|
20
|
+
* question answerable from one spot.
|
|
21
|
+
*/
|
|
22
|
+
export declare const WORKSPACE_PACKAGES: readonly ["@syncular/core", "@syncular/server", "@syncular/server-hono", "@syncular/client", "@syncular/typegen"];
|
|
23
|
+
/**
|
|
24
|
+
* The dependency range written into a scaffolded app's package.json when NOT
|
|
25
|
+
* scaffolding for in-tree testing (see {@link scaffoldApp}'s `local` flag).
|
|
26
|
+
*
|
|
27
|
+
* The v2 packages are unpublished and version-less today (all `private`, no
|
|
28
|
+
* `version` field — final identity + the release train is TODO 6.3). Until
|
|
29
|
+
* they publish there is no honest semver range to point at, so this is a
|
|
30
|
+
* `workspace:*`-shaped LOCAL default too, and the CLI warns loudly that a
|
|
31
|
+
* published install is not yet possible. When the packages ship, replace this
|
|
32
|
+
* with `^<version>` (or teach the CLI to read the published version) — one
|
|
33
|
+
* edit, here.
|
|
34
|
+
*/
|
|
35
|
+
export declare const PUBLISHED_DEPENDENCY_RANGE = "workspace:*";
|
|
36
|
+
/** Placeholder tokens substituted verbatim during scaffold (dumb + greppable). */
|
|
37
|
+
export declare const PLACEHOLDER: {
|
|
38
|
+
/** Replaced by the chosen project name in package.json/README. */
|
|
39
|
+
readonly projectName: "__PROJECT_NAME__";
|
|
40
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE place naming lives (TODO 6.3 is a product decision — final package
|
|
3
|
+
* identity is not settled). Every user-visible name the scaffolder emits comes
|
|
4
|
+
* from here so a rename is mechanical: change these constants, regenerate the
|
|
5
|
+
* templates' generated files, done. No name is hardcoded elsewhere in this
|
|
6
|
+
* package — grep for the literals below and you should only find them here.
|
|
7
|
+
*/
|
|
8
|
+
/** The scoped npm namespace for the v2 packages, e.g. `@syncular`. */
|
|
9
|
+
export const PACKAGE_SCOPE = '@syncular';
|
|
10
|
+
/** The typegen CLI binary name (`syncular generate`, `syncular init`). */
|
|
11
|
+
export const CLI_BIN = 'syncular';
|
|
12
|
+
/** This package's own bin (`bun create syncular …` / `bunx create-syncular-app …`). */
|
|
13
|
+
export const CREATE_BIN = 'create-syncular-app';
|
|
14
|
+
/** Human-facing product name in prose. */
|
|
15
|
+
export const PRODUCT_NAME = 'syncular';
|
|
16
|
+
/**
|
|
17
|
+
* The scoped workspace packages a template may depend on. The scaffolder only
|
|
18
|
+
* rewrites ranges for packages under {@link PACKAGE_SCOPE}, so this list is
|
|
19
|
+
* documentation, not a gate — but it keeps the "what does a template pull in"
|
|
20
|
+
* question answerable from one spot.
|
|
21
|
+
*/
|
|
22
|
+
export const WORKSPACE_PACKAGES = [
|
|
23
|
+
`${PACKAGE_SCOPE}/core`,
|
|
24
|
+
`${PACKAGE_SCOPE}/server`,
|
|
25
|
+
`${PACKAGE_SCOPE}/server-hono`,
|
|
26
|
+
`${PACKAGE_SCOPE}/client`,
|
|
27
|
+
`${PACKAGE_SCOPE}/typegen`,
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* The dependency range written into a scaffolded app's package.json when NOT
|
|
31
|
+
* scaffolding for in-tree testing (see {@link scaffoldApp}'s `local` flag).
|
|
32
|
+
*
|
|
33
|
+
* The v2 packages are unpublished and version-less today (all `private`, no
|
|
34
|
+
* `version` field — final identity + the release train is TODO 6.3). Until
|
|
35
|
+
* they publish there is no honest semver range to point at, so this is a
|
|
36
|
+
* `workspace:*`-shaped LOCAL default too, and the CLI warns loudly that a
|
|
37
|
+
* published install is not yet possible. When the packages ship, replace this
|
|
38
|
+
* with `^<version>` (or teach the CLI to read the published version) — one
|
|
39
|
+
* edit, here.
|
|
40
|
+
*/
|
|
41
|
+
export const PUBLISHED_DEPENDENCY_RANGE = 'workspace:*';
|
|
42
|
+
/** Placeholder tokens substituted verbatim during scaffold (dumb + greppable). */
|
|
43
|
+
export const PLACEHOLDER = {
|
|
44
|
+
/** Replaced by the chosen project name in package.json/README. */
|
|
45
|
+
projectName: '__PROJECT_NAME__',
|
|
46
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** The two templates this rung ships. */
|
|
2
|
+
export declare const TEMPLATES: readonly ["minimal", "web"];
|
|
3
|
+
export type TemplateName = (typeof TEMPLATES)[number];
|
|
4
|
+
export declare function isTemplateName(value: string): value is TemplateName;
|
|
5
|
+
/** Absolute path to `template/` inside this package (works for src or dist). */
|
|
6
|
+
export declare function templatesRoot(): string;
|
|
7
|
+
/** Derive a safe npm package name from the target directory basename. */
|
|
8
|
+
export declare function packageNameFromDirectory(targetDir: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Rewrite a template's package.json: set `name`, and resolve the dependency
|
|
11
|
+
* range for every `@<scope>/*` workspace dependency.
|
|
12
|
+
*
|
|
13
|
+
* The local-vs-published question, decided honestly (also documented in the
|
|
14
|
+
* package README):
|
|
15
|
+
* - `local: true` keeps `workspace:*` verbatim — the only ranges that resolve
|
|
16
|
+
* when the scaffolded app sits inside this repo's workspace (the in-tree
|
|
17
|
+
* smoke test path, and `--local` for anyone hacking on the tree).
|
|
18
|
+
* - `local: false` rewrites to {@link PUBLISHED_DEPENDENCY_RANGE}. Today that
|
|
19
|
+
* is *also* `workspace:*` because the v2 packages are unpublished and
|
|
20
|
+
* version-less (TODO 6.3); the CLI warns. It is one constant to flip the day
|
|
21
|
+
* they publish.
|
|
22
|
+
*/
|
|
23
|
+
export declare function rewriteTemplatePackageJson(source: string, options: {
|
|
24
|
+
packageName: string;
|
|
25
|
+
local: boolean;
|
|
26
|
+
}): string;
|
|
27
|
+
export interface ScaffoldOptions {
|
|
28
|
+
readonly template: TemplateName;
|
|
29
|
+
readonly targetDir: string;
|
|
30
|
+
/** Keep `workspace:*` ranges (in-tree testing / repo hacking). */
|
|
31
|
+
readonly local?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface ScaffoldResult {
|
|
34
|
+
readonly targetDir: string;
|
|
35
|
+
readonly packageName: string;
|
|
36
|
+
readonly template: TemplateName;
|
|
37
|
+
readonly local: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Copy `template/<template>` into `targetDir` and finalize it: restore the
|
|
41
|
+
* `.gitignore` name (npm strips real dotfiles from tarballs, so templates ship
|
|
42
|
+
* `gitignore`), rewrite package.json, and substitute placeholders.
|
|
43
|
+
*/
|
|
44
|
+
export declare function scaffoldApp(options: ScaffoldOptions): ScaffoldResult;
|