@sparkletree/cli 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 +68 -0
- package/dist/add.d.ts +38 -0
- package/dist/add.d.ts.map +1 -0
- package/dist/add.js +100 -0
- package/dist/add.js.map +1 -0
- package/dist/bin.d.ts +22 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +197 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +45 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +438 -0
- package/dist/registry.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SparkleTree
|
|
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,68 @@
|
|
|
1
|
+
# @sparkletree/cli
|
|
2
|
+
|
|
3
|
+
`sparkletree add` — copy SparkleTree component source into your repo, in the
|
|
4
|
+
shadcn/ui style. No install needed:
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npx @sparkletree/cli add hero cta
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Commands
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
sparkletree add <component...> [options]
|
|
14
|
+
sparkletree list
|
|
15
|
+
sparkletree help
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Option | |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `--dir <path>` | Where components are written. Default `components`. |
|
|
21
|
+
| `--cwd <path>` | Project root. Default: current directory. |
|
|
22
|
+
| `--force` | Overwrite files that already exist. |
|
|
23
|
+
| `--dry-run` | Show what would happen; write nothing. |
|
|
24
|
+
|
|
25
|
+
Dependencies are resolved for you — asking for `hero` also writes `provider` if
|
|
26
|
+
you do not have it.
|
|
27
|
+
|
|
28
|
+
## Components
|
|
29
|
+
|
|
30
|
+
| | |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `provider` | The `SparkletreeProvider` wrapper, reading your env vars. |
|
|
33
|
+
| `hero` | Streaming hero: headline, body, art. |
|
|
34
|
+
| `cta` | Streaming call-to-action. |
|
|
35
|
+
| `copy-card` | The copy-card creative. |
|
|
36
|
+
| `product-card` | Product creative with price and CTA. |
|
|
37
|
+
|
|
38
|
+
The copies require Tailwind CSS for their default classes. Swap them freely —
|
|
39
|
+
they are yours.
|
|
40
|
+
|
|
41
|
+
## What "yours" means, and the one thing it does not include
|
|
42
|
+
|
|
43
|
+
Copied components are your code: we cannot patch them, deprecate them, or know
|
|
44
|
+
what they look like a year from now. So they are **wire-ignorant by
|
|
45
|
+
construction** — no event names, no protocol versions, no transports, no
|
|
46
|
+
endpoints, no timing constants. A CI test enforces this per component.
|
|
47
|
+
|
|
48
|
+
Everything that speaks the wire stays in
|
|
49
|
+
[`@sparkletree/react`](https://www.npmjs.com/package/@sparkletree/react) and
|
|
50
|
+
[`@sparkletree/core`](https://www.npmjs.com/package/@sparkletree/core), which
|
|
51
|
+
the copies import from. That is what buys the property that makes copy-paste
|
|
52
|
+
distribution safe: **a protocol change is a version bump, never a migration
|
|
53
|
+
guide.**
|
|
54
|
+
|
|
55
|
+
It also means the adaptation mark is not in the copied source. It renders
|
|
56
|
+
inside `StreamText`, in npm, and deleting it from your copy is not possible
|
|
57
|
+
because it was never there. That is deliberate — see rule 4 in the
|
|
58
|
+
[SDK README](https://gitlab.com/sparkletree/sparkletree-sdk/-/blob/main/README.md).
|
|
59
|
+
|
|
60
|
+
## Choose one distribution, not both
|
|
61
|
+
|
|
62
|
+
Either import components from `@sparkletree/react`, or copy them with this CLI
|
|
63
|
+
and import from `./components/sparkletree/…`. Doing both gives you two Heroes
|
|
64
|
+
with the same name and a confusing diff.
|
|
65
|
+
|
|
66
|
+
## Requirements
|
|
67
|
+
|
|
68
|
+
Node >= 18. ESM only.
|
package/dist/add.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sparkletree add <component…>`
|
|
3
|
+
*
|
|
4
|
+
* Copies component source into the project. Everything about how it behaves
|
|
5
|
+
* follows from one principle: THIS WRITES TO SOMEONE ELSE'S REPOSITORY.
|
|
6
|
+
*
|
|
7
|
+
* - never overwrite without `--force`. The file may be three months of the
|
|
8
|
+
* customer's edits, and "the CLI reverted my hero" is unrecoverable if we
|
|
9
|
+
* wrote first and asked later;
|
|
10
|
+
* - report the dependencies to install rather than running a package
|
|
11
|
+
* manager. Which one, which lockfile, which workspace flag — those are
|
|
12
|
+
* theirs to decide, and guessing wrong dirties a lockfile in a PR;
|
|
13
|
+
* - `--dry-run` shows exactly what would happen and touches nothing.
|
|
14
|
+
*/
|
|
15
|
+
import { type RegistryComponent } from "./registry.js";
|
|
16
|
+
export interface AddOptions {
|
|
17
|
+
/** Project root. Defaults to the working directory. */
|
|
18
|
+
cwd?: string;
|
|
19
|
+
/** Where components land, relative to cwd. */
|
|
20
|
+
targetDir?: string;
|
|
21
|
+
force?: boolean;
|
|
22
|
+
dryRun?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface AddResult {
|
|
25
|
+
written: string[];
|
|
26
|
+
skipped: {
|
|
27
|
+
path: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
}[];
|
|
30
|
+
dependencies: string[];
|
|
31
|
+
components: RegistryComponent[];
|
|
32
|
+
}
|
|
33
|
+
export declare function add(names: string[], options?: AddOptions): Promise<AddResult>;
|
|
34
|
+
/** Human-readable summary. Mirrors what `add` actually did. */
|
|
35
|
+
export declare function formatAddResult(result: AddResult, dryRun?: boolean): string;
|
|
36
|
+
/** Where `add` would write, for `--help` and for tests. */
|
|
37
|
+
export declare function targetPathFor(component: RegistryComponent, targetDir?: string): string;
|
|
38
|
+
//# sourceMappingURL=add.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../src/add.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,EAA2B,KAAK,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEhF,MAAM,WAAW,UAAU;IACzB,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5C,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,iBAAiB,EAAE,CAAC;CACjC;AAYD,wBAAsB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAqDvF;AAED,+DAA+D;AAC/D,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,UAAQ,GAAG,MAAM,CA6BzE;AAED,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,SAAS,EAAE,iBAAiB,EAAE,SAAS,SAAe,GAAG,MAAM,CAE5F"}
|
package/dist/add.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sparkletree add <component…>`
|
|
3
|
+
*
|
|
4
|
+
* Copies component source into the project. Everything about how it behaves
|
|
5
|
+
* follows from one principle: THIS WRITES TO SOMEONE ELSE'S REPOSITORY.
|
|
6
|
+
*
|
|
7
|
+
* - never overwrite without `--force`. The file may be three months of the
|
|
8
|
+
* customer's edits, and "the CLI reverted my hero" is unrecoverable if we
|
|
9
|
+
* wrote first and asked later;
|
|
10
|
+
* - report the dependencies to install rather than running a package
|
|
11
|
+
* manager. Which one, which lockfile, which workspace flag — those are
|
|
12
|
+
* theirs to decide, and guessing wrong dirties a lockfile in a PR;
|
|
13
|
+
* - `--dry-run` shows exactly what would happen and touches nothing.
|
|
14
|
+
*/
|
|
15
|
+
import { mkdir, stat, writeFile } from "node:fs/promises";
|
|
16
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
17
|
+
import { resolveWithDependencies } from "./registry.js";
|
|
18
|
+
/** What already sits at a path, without reading its contents. */
|
|
19
|
+
async function pathKind(path) {
|
|
20
|
+
try {
|
|
21
|
+
const info = await stat(path);
|
|
22
|
+
return info.isDirectory() ? "directory" : "file";
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function add(names, options = {}) {
|
|
29
|
+
const cwd = options.cwd ?? process.cwd();
|
|
30
|
+
const targetDir = options.targetDir ?? "components";
|
|
31
|
+
const components = resolveWithDependencies(names);
|
|
32
|
+
const written = [];
|
|
33
|
+
const skipped = [];
|
|
34
|
+
const dependencies = new Set();
|
|
35
|
+
for (const component of components) {
|
|
36
|
+
for (const dependency of component.dependencies)
|
|
37
|
+
dependencies.add(dependency);
|
|
38
|
+
for (const file of component.files) {
|
|
39
|
+
const absolute = resolve(cwd, targetDir, file.path);
|
|
40
|
+
// Refuse to escape the target directory. Registry paths are ours today,
|
|
41
|
+
// but a path-traversal check on a tool that writes files is not
|
|
42
|
+
// something to add after it matters. The check is against ".." as a
|
|
43
|
+
// path segment — a filename that merely STARTS with dots ("..foo.tsx")
|
|
44
|
+
// is still inside the target and must not trip it.
|
|
45
|
+
const relativeToTarget = relative(resolve(cwd, targetDir), absolute);
|
|
46
|
+
if (relativeToTarget.startsWith(".." + sep) || isAbsolute(relativeToTarget)) {
|
|
47
|
+
throw new Error(`Registry path escapes the target directory: ${file.path}`);
|
|
48
|
+
}
|
|
49
|
+
// A directory at the target path is never overwritten, --force or not:
|
|
50
|
+
// writeFile would die with a raw EISDIR, and recursive removal of a
|
|
51
|
+
// customer's directory is not this tool's call to make.
|
|
52
|
+
const existing = await pathKind(absolute);
|
|
53
|
+
if (existing === "directory") {
|
|
54
|
+
skipped.push({
|
|
55
|
+
path: relative(cwd, absolute),
|
|
56
|
+
reason: "a directory exists at this path — remove it or choose another --dir",
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!options.force && existing === "file") {
|
|
61
|
+
skipped.push({
|
|
62
|
+
path: relative(cwd, absolute),
|
|
63
|
+
reason: "already exists — pass --force to overwrite",
|
|
64
|
+
});
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!options.dryRun) {
|
|
68
|
+
await mkdir(dirname(absolute), { recursive: true });
|
|
69
|
+
await writeFile(absolute, file.content, "utf8");
|
|
70
|
+
}
|
|
71
|
+
written.push(relative(cwd, absolute));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { written, skipped, dependencies: [...dependencies], components };
|
|
75
|
+
}
|
|
76
|
+
/** Human-readable summary. Mirrors what `add` actually did. */
|
|
77
|
+
export function formatAddResult(result, dryRun = false) {
|
|
78
|
+
const lines = [];
|
|
79
|
+
const verb = dryRun ? "Would write" : "Wrote";
|
|
80
|
+
if (result.written.length) {
|
|
81
|
+
lines.push(`${verb}:`);
|
|
82
|
+
for (const path of result.written)
|
|
83
|
+
lines.push(` ${path}`);
|
|
84
|
+
}
|
|
85
|
+
if (result.skipped.length) {
|
|
86
|
+
lines.push("", "Skipped:");
|
|
87
|
+
for (const item of result.skipped)
|
|
88
|
+
lines.push(` ${item.path} — ${item.reason}`);
|
|
89
|
+
}
|
|
90
|
+
if (result.dependencies.length) {
|
|
91
|
+
lines.push("", "Install these if you haven't already:", ` ${result.dependencies.join(" ")}`);
|
|
92
|
+
}
|
|
93
|
+
lines.push("", "These files are yours — edit them freely. Streaming, timing and the", "adaptation mark live in @sparkletree/core, so upgrades reach you without", "re-copying anything.", "", "The default classNames assume Tailwind CSS — swap them freely if you", "style with something else.");
|
|
94
|
+
return lines.join("\n");
|
|
95
|
+
}
|
|
96
|
+
/** Where `add` would write, for `--help` and for tests. */
|
|
97
|
+
export function targetPathFor(component, targetDir = "components") {
|
|
98
|
+
return join(targetDir, component.files[0].path);
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=add.js.map
|
package/dist/add.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add.js","sourceRoot":"","sources":["../src/add.ts"],"names":[],"mappings":"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"}
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `sparkletree` binary.
|
|
4
|
+
*
|
|
5
|
+
* Argument parsing by hand rather than a dependency: this CLI has three
|
|
6
|
+
* commands and a handful of flags, and a customer running `npx @sparkletree/cli
|
|
7
|
+
* add hero` should not be installing a parser to do it.
|
|
8
|
+
*/
|
|
9
|
+
interface ParsedArgs {
|
|
10
|
+
command: string;
|
|
11
|
+
names: string[];
|
|
12
|
+
dir?: string;
|
|
13
|
+
cwd?: string;
|
|
14
|
+
force: boolean;
|
|
15
|
+
dryRun: boolean;
|
|
16
|
+
help: boolean;
|
|
17
|
+
version: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
20
|
+
declare function main(argv: string[]): Promise<number>;
|
|
21
|
+
export { main };
|
|
22
|
+
//# sourceMappingURL=bin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AA6BH,UAAU,UAAU;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAeD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAuBpD;AAaD,iBAAe,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuEnD;AA6CD,OAAO,EAAE,IAAI,EAAE,CAAC"}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `sparkletree` binary.
|
|
4
|
+
*
|
|
5
|
+
* Argument parsing by hand rather than a dependency: this CLI has three
|
|
6
|
+
* commands and a handful of flags, and a customer running `npx @sparkletree/cli
|
|
7
|
+
* add hero` should not be installing a parser to do it.
|
|
8
|
+
*/
|
|
9
|
+
import { realpathSync, readFileSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { add, formatAddResult } from "./add.js";
|
|
13
|
+
import { REGISTRY, findComponent } from "./registry.js";
|
|
14
|
+
const HELP = `sparkletree — add SparkleTree components to your project
|
|
15
|
+
|
|
16
|
+
Usage
|
|
17
|
+
sparkletree add <component...> [options]
|
|
18
|
+
sparkletree list
|
|
19
|
+
sparkletree help
|
|
20
|
+
|
|
21
|
+
Options
|
|
22
|
+
--dir <path> where components are written (default: components)
|
|
23
|
+
--cwd <path> project root (default: current directory)
|
|
24
|
+
--force overwrite files that already exist
|
|
25
|
+
--dry-run show what would happen; write nothing
|
|
26
|
+
-h, --help show this help
|
|
27
|
+
--version show the CLI version
|
|
28
|
+
|
|
29
|
+
Examples
|
|
30
|
+
npx @sparkletree/cli add hero cta
|
|
31
|
+
npx @sparkletree/cli add product-card --dir src/components
|
|
32
|
+
`;
|
|
33
|
+
/**
|
|
34
|
+
* The value after a value-taking flag. `--dir --force` must fail loudly, not
|
|
35
|
+
* write into a directory literally named "--force"; a trailing `--dir` must
|
|
36
|
+
* fail loudly, not become `undefined` and silently mean the default.
|
|
37
|
+
*/
|
|
38
|
+
function optionValue(flag, rest, index) {
|
|
39
|
+
const value = rest[index];
|
|
40
|
+
if (value === undefined || value.startsWith("--")) {
|
|
41
|
+
throw new Error(`Option ${flag} needs a value, got ${value ?? "end of arguments"}`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
export function parseArgs(argv) {
|
|
46
|
+
const [command = "help", ...rest] = argv;
|
|
47
|
+
const parsed = {
|
|
48
|
+
command,
|
|
49
|
+
names: [],
|
|
50
|
+
force: false,
|
|
51
|
+
dryRun: false,
|
|
52
|
+
help: false,
|
|
53
|
+
version: false,
|
|
54
|
+
};
|
|
55
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
56
|
+
const arg = rest[index];
|
|
57
|
+
if (arg === "--force")
|
|
58
|
+
parsed.force = true;
|
|
59
|
+
else if (arg === "--dry-run")
|
|
60
|
+
parsed.dryRun = true;
|
|
61
|
+
else if (arg === "--help" || arg === "-h")
|
|
62
|
+
parsed.help = true;
|
|
63
|
+
else if (arg === "--version")
|
|
64
|
+
parsed.version = true;
|
|
65
|
+
else if (arg === "--dir")
|
|
66
|
+
parsed.dir = optionValue("--dir", rest, ++index);
|
|
67
|
+
else if (arg === "--cwd")
|
|
68
|
+
parsed.cwd = optionValue("--cwd", rest, ++index);
|
|
69
|
+
else if (arg.startsWith("--"))
|
|
70
|
+
throw new Error(`Unknown option ${arg}`);
|
|
71
|
+
else
|
|
72
|
+
parsed.names.push(arg);
|
|
73
|
+
}
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
/** The CLI's own version, read from the package.json the build ships next to. */
|
|
77
|
+
function cliVersion() {
|
|
78
|
+
try {
|
|
79
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
80
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
81
|
+
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return "unknown";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function main(argv) {
|
|
88
|
+
let args;
|
|
89
|
+
try {
|
|
90
|
+
args = parseArgs(argv);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
94
|
+
console.error("\n" + HELP);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
// --help and --version work from any position: `sparkletree add --help`
|
|
98
|
+
// shows usage rather than dying on an "unknown option".
|
|
99
|
+
if (args.help) {
|
|
100
|
+
console.log(HELP);
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
if (args.version) {
|
|
104
|
+
console.log(cliVersion());
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
switch (args.command) {
|
|
108
|
+
case "list": {
|
|
109
|
+
console.log("Available components:\n");
|
|
110
|
+
for (const component of REGISTRY) {
|
|
111
|
+
console.log(` ${component.name.padEnd(14)} ${component.description}`);
|
|
112
|
+
}
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
case "add": {
|
|
116
|
+
if (!args.names.length) {
|
|
117
|
+
console.error("Which component? Try `sparkletree list`.\n");
|
|
118
|
+
console.error(HELP);
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
const unknown = args.names.filter((name) => !findComponent(name));
|
|
122
|
+
if (unknown.length) {
|
|
123
|
+
console.error(`Unknown component(s): ${unknown.join(", ")}`);
|
|
124
|
+
console.error(`Available: ${REGISTRY.map((c) => c.name).join(", ")}`);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
const result = await add(args.names, {
|
|
128
|
+
cwd: args.cwd,
|
|
129
|
+
targetDir: args.dir,
|
|
130
|
+
force: args.force,
|
|
131
|
+
dryRun: args.dryRun,
|
|
132
|
+
});
|
|
133
|
+
console.log(formatAddResult(result, args.dryRun));
|
|
134
|
+
// Skipped files are not a failure: re-running `add` on a project that
|
|
135
|
+
// already has the component is a normal thing to do, and exiting
|
|
136
|
+
// non-zero would break it in CI for no reason.
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
case "help":
|
|
140
|
+
case "--help":
|
|
141
|
+
case "-h":
|
|
142
|
+
console.log(HELP);
|
|
143
|
+
return 0;
|
|
144
|
+
case "--version":
|
|
145
|
+
console.log(cliVersion());
|
|
146
|
+
return 0;
|
|
147
|
+
default:
|
|
148
|
+
console.error(`Unknown command "${args.command}"\n`);
|
|
149
|
+
console.error(HELP);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Are we the entry point, or is something importing us?
|
|
155
|
+
*
|
|
156
|
+
* Importing this module (tests, programmatic use) must not run a command or
|
|
157
|
+
* call process.exit — but the check has to survive how package managers
|
|
158
|
+
* actually install a `bin`.
|
|
159
|
+
*
|
|
160
|
+
* npm installs it as a bare SYMLINK: `node_modules/.bin/sparkletree` →
|
|
161
|
+
* `dist/bin.js`. Node then resolves `import.meta.url` through the symlink to
|
|
162
|
+
* the real file while leaving `process.argv[1]` as the symlink path. Comparing
|
|
163
|
+
* the two textually — which this used to do, against argv[1]'s basename —
|
|
164
|
+
* asks whether "…/dist/bin.js" ends with "sparkletree". It does not, so
|
|
165
|
+
* `npx sparkletree` parsed nothing, printed nothing, and exited 0. A silent
|
|
166
|
+
* no-op on the one command every consumer runs first.
|
|
167
|
+
*
|
|
168
|
+
* (pnpm masked it: pnpm writes a shell shim that execs the real path, so
|
|
169
|
+
* argv[1] and import.meta.url already agree. So do all our own tests, which
|
|
170
|
+
* import this module rather than spawning the installed binary.)
|
|
171
|
+
*
|
|
172
|
+
* realpath both sides and compare. Symlink, shim or direct — they agree.
|
|
173
|
+
*/
|
|
174
|
+
function isEntryPoint() {
|
|
175
|
+
const entry = process.argv[1];
|
|
176
|
+
if (!entry)
|
|
177
|
+
return false;
|
|
178
|
+
try {
|
|
179
|
+
return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// A deleted or unreadable argv[1] is not us.
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (isEntryPoint()) {
|
|
187
|
+
main(process.argv.slice(2))
|
|
188
|
+
.then((code) => {
|
|
189
|
+
process.exitCode = code;
|
|
190
|
+
})
|
|
191
|
+
.catch((error) => {
|
|
192
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
export { main };
|
|
197
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sparkletree/cli — programmatic surface.
|
|
3
|
+
*
|
|
4
|
+
* The binary is a thin wrapper over these. Exported so the parity harness and
|
|
5
|
+
* anyone scripting a scaffold can use `add` without shelling out.
|
|
6
|
+
*/
|
|
7
|
+
export { add, formatAddResult, targetPathFor, type AddOptions, type AddResult } from "./add.js";
|
|
8
|
+
export { REGISTRY, findComponent, resolveWithDependencies, type RegistryComponent, } from "./registry.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAChG,OAAO,EACL,QAAQ,EACR,aAAa,EACb,uBAAuB,EACvB,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sparkletree/cli — programmatic surface.
|
|
3
|
+
*
|
|
4
|
+
* The binary is a thin wrapper over these. Exported so the parity harness and
|
|
5
|
+
* anyone scripting a scaffold can use `add` without shelling out.
|
|
6
|
+
*/
|
|
7
|
+
export { add, formatAddResult, targetPathFor } from "./add.js";
|
|
8
|
+
export { REGISTRY, findComponent, resolveWithDependencies, } from "./registry.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,aAAa,EAAmC,MAAM,UAAU,CAAC;AAChG,OAAO,EACL,QAAQ,EACR,aAAa,EACb,uBAAuB,GAExB,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The component registry.
|
|
3
|
+
*
|
|
4
|
+
* `sparkletree add hero` copies source into the customer's repo, shadcn-style.
|
|
5
|
+
* Those copies are theirs: we cannot patch them, cannot deprecate them, and
|
|
6
|
+
* cannot assume anything about what they have become by the time we ship the
|
|
7
|
+
* next protocol version.
|
|
8
|
+
*
|
|
9
|
+
* Which is the whole reason for the rule enforced in `registry.test.ts`:
|
|
10
|
+
*
|
|
11
|
+
* COPIES ARE WIRE-IGNORANT. ONLY NPM SPEAKS THE WIRE.
|
|
12
|
+
*
|
|
13
|
+
* A copied component may import from `@sparkletree/react` and
|
|
14
|
+
* `@sparkletree/core`. It may not contain an event name, a protocol version,
|
|
15
|
+
* a timing constant, or a URL. The moment one does, a protocol change stops
|
|
16
|
+
* being a version bump and becomes a migration guide addressed to every
|
|
17
|
+
* customer who ever ran `add` — and they will not all read it.
|
|
18
|
+
*
|
|
19
|
+
* The registry ships as data, not as files fetched at install time: a CLI that
|
|
20
|
+
* pulls source off the network at `add` time is a supply-chain hole in every
|
|
21
|
+
* customer's build.
|
|
22
|
+
*/
|
|
23
|
+
export interface RegistryComponent {
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
/** Files written, relative to the target directory. */
|
|
27
|
+
files: {
|
|
28
|
+
path: string;
|
|
29
|
+
content: string;
|
|
30
|
+
}[];
|
|
31
|
+
/** npm packages the copy needs. */
|
|
32
|
+
dependencies: string[];
|
|
33
|
+
/** Other registry components this one requires. */
|
|
34
|
+
registryDependencies: string[];
|
|
35
|
+
}
|
|
36
|
+
export declare const REGISTRY: readonly RegistryComponent[];
|
|
37
|
+
export declare function findComponent(name: string): RegistryComponent | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a component plus everything it needs, dependencies first.
|
|
40
|
+
*
|
|
41
|
+
* Depth-first with a visited set, so a diamond in the graph copies each file
|
|
42
|
+
* once and a cycle terminates rather than recursing until the stack gives out.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveWithDependencies(names: string[]): RegistryComponent[];
|
|
45
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3C,mCAAmC;IACnC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mDAAmD;IACnD,oBAAoB,EAAE,MAAM,EAAE,CAAC;CAChC;AAsWD,eAAO,MAAM,QAAQ,EAAE,SAAS,iBAAiB,EAoChD,CAAC;AAEF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAEzE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,iBAAiB,EAAE,CAe5E"}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The component registry.
|
|
3
|
+
*
|
|
4
|
+
* `sparkletree add hero` copies source into the customer's repo, shadcn-style.
|
|
5
|
+
* Those copies are theirs: we cannot patch them, cannot deprecate them, and
|
|
6
|
+
* cannot assume anything about what they have become by the time we ship the
|
|
7
|
+
* next protocol version.
|
|
8
|
+
*
|
|
9
|
+
* Which is the whole reason for the rule enforced in `registry.test.ts`:
|
|
10
|
+
*
|
|
11
|
+
* COPIES ARE WIRE-IGNORANT. ONLY NPM SPEAKS THE WIRE.
|
|
12
|
+
*
|
|
13
|
+
* A copied component may import from `@sparkletree/react` and
|
|
14
|
+
* `@sparkletree/core`. It may not contain an event name, a protocol version,
|
|
15
|
+
* a timing constant, or a URL. The moment one does, a protocol change stops
|
|
16
|
+
* being a version bump and becomes a migration guide addressed to every
|
|
17
|
+
* customer who ever ran `add` — and they will not all read it.
|
|
18
|
+
*
|
|
19
|
+
* The registry ships as data, not as files fetched at install time: a CLI that
|
|
20
|
+
* pulls source off the network at `add` time is a supply-chain hole in every
|
|
21
|
+
* customer's build.
|
|
22
|
+
*/
|
|
23
|
+
const HERO_TSX = `"use client";
|
|
24
|
+
|
|
25
|
+
import { StreamText, useIsland, type UseIslandOptions } from "@sparkletree/react";
|
|
26
|
+
import { themeStyle } from "@sparkletree/core";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Your hero. Edit freely — this file is yours.
|
|
30
|
+
*
|
|
31
|
+
* What you should NOT need to touch, and what you will not find here: when the
|
|
32
|
+
* copy paints, how long it holds, whether it types or lands composed, and
|
|
33
|
+
* whether it carries the adaptation mark. All of that lives in
|
|
34
|
+
* @sparkletree/core and @sparkletree/react so it can be fixed for you without
|
|
35
|
+
* you re-copying this file.
|
|
36
|
+
*
|
|
37
|
+
* \`fallback\` is required. There is no loading state and no skeleton: if the
|
|
38
|
+
* network is slow or the campaign is unreachable, this copy is what your
|
|
39
|
+
* visitor reads. Write it as if it were the only version — often it is.
|
|
40
|
+
*
|
|
41
|
+
* Requires Tailwind CSS for the default classes — swap them freely.
|
|
42
|
+
*/
|
|
43
|
+
export interface HeroProps {
|
|
44
|
+
campaignId: string;
|
|
45
|
+
/** First paint. Real copy, never a placeholder. */
|
|
46
|
+
fallback: UseIslandOptions["fallback"];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Server-driven URLs are configuration, not trust. Only http(s), mailto and
|
|
51
|
+
* tel absolutes and relative navigations ("/", "./", "#", "?") pass; anything
|
|
52
|
+
* else — javascript:, data:, an unprefixed string — is dropped.
|
|
53
|
+
*/
|
|
54
|
+
function safeActionUrl(url: string | null | undefined): string | undefined {
|
|
55
|
+
if (!url) return undefined;
|
|
56
|
+
if (
|
|
57
|
+
(url.startsWith("/") && !url.startsWith("//")) ||
|
|
58
|
+
url.startsWith("./") ||
|
|
59
|
+
url.startsWith("#") ||
|
|
60
|
+
url.startsWith("?")
|
|
61
|
+
) {
|
|
62
|
+
return url;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const protocol = new URL(url).protocol;
|
|
66
|
+
if (["https:", "http:", "mailto:", "tel:"].includes(protocol)) return url;
|
|
67
|
+
} catch {
|
|
68
|
+
// Not a parseable URL at all.
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function Hero({ campaignId, fallback }: HeroProps) {
|
|
74
|
+
const { state, ref } = useIsland({ campaignId, island: "hero", fallback });
|
|
75
|
+
const backgroundImage = safeActionUrl(state.backgroundImage);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<section
|
|
79
|
+
ref={ref}
|
|
80
|
+
className="relative overflow-hidden rounded-2xl p-10"
|
|
81
|
+
style={themeStyle(state.theme)}
|
|
82
|
+
>
|
|
83
|
+
{backgroundImage ? (
|
|
84
|
+
<img
|
|
85
|
+
src={backgroundImage}
|
|
86
|
+
alt=""
|
|
87
|
+
className="absolute inset-0 h-full w-full object-cover"
|
|
88
|
+
/>
|
|
89
|
+
) : null}
|
|
90
|
+
|
|
91
|
+
<div className="relative">
|
|
92
|
+
{state.fields.greeting.text ? (
|
|
93
|
+
<StreamText
|
|
94
|
+
field={state.fields.greeting}
|
|
95
|
+
as="p"
|
|
96
|
+
className="text-sm uppercase tracking-wide opacity-80"
|
|
97
|
+
/>
|
|
98
|
+
) : null}
|
|
99
|
+
|
|
100
|
+
{/* h2 to match the packaged Hero. The heading level is yours: pass
|
|
101
|
+
as="h1" if this section is your page's primary heading. */}
|
|
102
|
+
<StreamText
|
|
103
|
+
field={state.fields.headline}
|
|
104
|
+
as="h2"
|
|
105
|
+
className="mt-2 text-4xl font-semibold"
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
{state.fields.body.text ? (
|
|
109
|
+
<StreamText field={state.fields.body} as="p" className="mt-4 max-w-prose" />
|
|
110
|
+
) : null}
|
|
111
|
+
</div>
|
|
112
|
+
</section>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
const CTA_TSX = `"use client";
|
|
117
|
+
|
|
118
|
+
import { StreamText, useIsland, useSparkletree, type UseIslandOptions } from "@sparkletree/react";
|
|
119
|
+
import { readableTextColor, themeStyle, track } from "@sparkletree/core";
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Your call to action. Edit freely.
|
|
123
|
+
*
|
|
124
|
+
* The button keeps its size while the label streams in, so the surrounding
|
|
125
|
+
* layout does not shift under the visitor's cursor mid-click.
|
|
126
|
+
*
|
|
127
|
+
* Requires Tailwind CSS for the default classes — swap them freely.
|
|
128
|
+
*/
|
|
129
|
+
export interface CtaProps extends Omit<UseIslandOptions, "island"> {
|
|
130
|
+
/** Where the button goes when the server has not resolved an action. */
|
|
131
|
+
href?: string;
|
|
132
|
+
onClick?: () => void;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Server-driven URLs are configuration, not trust. Only http(s), mailto and
|
|
137
|
+
* tel absolutes and relative navigations ("/", "./", "#", "?") pass; anything
|
|
138
|
+
* else — javascript:, data:, an unprefixed string — is dropped.
|
|
139
|
+
*/
|
|
140
|
+
function safeActionUrl(url: string | null | undefined): string | undefined {
|
|
141
|
+
if (!url) return undefined;
|
|
142
|
+
if (
|
|
143
|
+
(url.startsWith("/") && !url.startsWith("//")) ||
|
|
144
|
+
url.startsWith("./") ||
|
|
145
|
+
url.startsWith("#") ||
|
|
146
|
+
url.startsWith("?")
|
|
147
|
+
) {
|
|
148
|
+
return url;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const protocol = new URL(url).protocol;
|
|
152
|
+
if (["https:", "http:", "mailto:", "tel:"].includes(protocol)) return url;
|
|
153
|
+
} catch {
|
|
154
|
+
// Not a parseable URL at all.
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function Cta(props: CtaProps) {
|
|
160
|
+
const { href, onClick, ...islandOptions } = props;
|
|
161
|
+
const { apiBase, organizationId } = useSparkletree();
|
|
162
|
+
const { state, ref } = useIsland({ ...islandOptions, island: "cta" });
|
|
163
|
+
// A rejected server action falls back to the href prop you control. The
|
|
164
|
+
// prop is vetted too — the same render path, the same rules, one outcome.
|
|
165
|
+
const action = safeActionUrl(state.ctaAction) ?? safeActionUrl(href);
|
|
166
|
+
const { campaignId, surfaceId } = islandOptions;
|
|
167
|
+
const variantId = state.variantId ?? undefined;
|
|
168
|
+
// A surface-only Cta has no campaignId prop, but the click must still be
|
|
169
|
+
// attributed: the server names the campaign it served in the terminal
|
|
170
|
+
// done.meta, mirroring the impression path in useIsland.
|
|
171
|
+
const metaCampaignId =
|
|
172
|
+
typeof state.meta?.campaignId === "string" ? state.meta.campaignId : undefined;
|
|
173
|
+
const resolvedCampaignId = campaignId ?? metaCampaignId;
|
|
174
|
+
|
|
175
|
+
// The click is the other half of CTR. The SDK tells the server to suppress
|
|
176
|
+
// its own counting for this page view — so if nothing here reports the
|
|
177
|
+
// click, CTR for SDK traffic is a division by silence. Fire-and-forget:
|
|
178
|
+
// track never throws and never blocks the navigation.
|
|
179
|
+
const handleClick = () => {
|
|
180
|
+
if (resolvedCampaignId) {
|
|
181
|
+
void track(
|
|
182
|
+
{ apiBase, organizationId, campaignId: resolvedCampaignId, variantId, surfaceId },
|
|
183
|
+
"click",
|
|
184
|
+
{ island: "cta" },
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
onClick?.();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Shared by the link and the no-destination fallback below.
|
|
191
|
+
const shared = {
|
|
192
|
+
onClick: handleClick,
|
|
193
|
+
className:
|
|
194
|
+
"inline-flex min-h-11 min-w-40 items-center justify-center rounded-full px-6 font-medium",
|
|
195
|
+
style: {
|
|
196
|
+
...themeStyle(state.theme),
|
|
197
|
+
// --st-cta-text is the label ink that reads on the theme's primary, so
|
|
198
|
+
// a light theme can never produce a ghost button.
|
|
199
|
+
...(state.theme?.colors?.primary
|
|
200
|
+
? { ["--st-cta-text" as string]: readableTextColor(state.theme.colors.primary) }
|
|
201
|
+
: {}),
|
|
202
|
+
},
|
|
203
|
+
// A CTA whose label is still empty is not yet a button a viewer can
|
|
204
|
+
// meaningfully act on — but it must still occupy its space, or the
|
|
205
|
+
// layout shifts when the copy lands.
|
|
206
|
+
"aria-disabled": state.fields.cta.text ? undefined : true,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Neither the stream nor you resolved a destination: render a
|
|
210
|
+
// non-interactive element rather than an inert <a> with no href, which
|
|
211
|
+
// some assistive tech still announces as a link to nowhere. Clicks are
|
|
212
|
+
// still observed — engagement with the creative counts toward CTR whether
|
|
213
|
+
// or not it navigates.
|
|
214
|
+
if (action === undefined) {
|
|
215
|
+
return (
|
|
216
|
+
<span ref={ref as unknown as (element: HTMLSpanElement | null) => void} {...shared}>
|
|
217
|
+
<StreamText field={state.fields.cta} />
|
|
218
|
+
</span>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<a ref={ref} href={action} {...shared}>
|
|
224
|
+
<StreamText field={state.fields.cta} />
|
|
225
|
+
</a>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
`;
|
|
229
|
+
const COPY_CARD_TSX = `import { StreamText, staticField, type ContentSource } from "@sparkletree/react";
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* A copy card fed by /content rather than a stream.
|
|
233
|
+
*
|
|
234
|
+
* \`contentSource\` comes from the /content response's meta. Pass it through —
|
|
235
|
+
* do not default it to "generated" to get the mark, and do not hardcode
|
|
236
|
+
* "static" to lose it. The mark follows the copy's provenance, and both
|
|
237
|
+
* shortcuts make it say something untrue.
|
|
238
|
+
*
|
|
239
|
+
* \`staticField\` builds the field object for you. Keep using it rather than
|
|
240
|
+
* writing the object literal yourself: the field's shape belongs to the npm
|
|
241
|
+
* package, and the helper is what lets it grow without breaking this file.
|
|
242
|
+
*
|
|
243
|
+
* Requires Tailwind CSS for the default classes — swap them freely.
|
|
244
|
+
*/
|
|
245
|
+
export interface CopyCardProps {
|
|
246
|
+
headline?: string;
|
|
247
|
+
body?: string;
|
|
248
|
+
/** Provenance of this copy, from the /content response's meta. */
|
|
249
|
+
contentSource?: ContentSource;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function CopyCard({ headline, body, contentSource = "static" }: CopyCardProps) {
|
|
253
|
+
return (
|
|
254
|
+
<article className="rounded-xl border p-6">
|
|
255
|
+
<StreamText
|
|
256
|
+
field={staticField(headline, contentSource)}
|
|
257
|
+
as="h3"
|
|
258
|
+
className="text-lg font-semibold"
|
|
259
|
+
/>
|
|
260
|
+
{body ? (
|
|
261
|
+
<StreamText field={staticField(body, contentSource)} as="p" className="mt-2 opacity-80" />
|
|
262
|
+
) : null}
|
|
263
|
+
</article>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
`;
|
|
267
|
+
const PRODUCT_CARD_TSX = `import { StreamText, staticField, type ContentSource } from "@sparkletree/react";
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* A product card.
|
|
271
|
+
*
|
|
272
|
+
* Note that the name and price are always "static": they come from your
|
|
273
|
+
* catalogue, not from a model. Only the description can be adapted, so only
|
|
274
|
+
* the description can carry the mark — marking a price as adapted would be a
|
|
275
|
+
* straightforward falsehood about the price.
|
|
276
|
+
*
|
|
277
|
+
* \`staticField\` builds the field object for you. Keep using it rather than
|
|
278
|
+
* writing the object literal yourself: the field's shape belongs to the npm
|
|
279
|
+
* package, and the helper is what lets it grow without breaking this file.
|
|
280
|
+
*
|
|
281
|
+
* Requires Tailwind CSS for the default classes — swap them freely.
|
|
282
|
+
*/
|
|
283
|
+
export interface ProductCardProps {
|
|
284
|
+
product: {
|
|
285
|
+
name: string;
|
|
286
|
+
description?: string;
|
|
287
|
+
price?: string | number;
|
|
288
|
+
imageUrl?: string;
|
|
289
|
+
};
|
|
290
|
+
/** Provenance of the description. The name and price are never generated. */
|
|
291
|
+
contentSource?: ContentSource;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function ProductCard({ product, contentSource = "static" }: ProductCardProps) {
|
|
295
|
+
return (
|
|
296
|
+
<article className="rounded-xl border p-4">
|
|
297
|
+
{product.imageUrl ? (
|
|
298
|
+
<img
|
|
299
|
+
src={product.imageUrl}
|
|
300
|
+
alt={product.name}
|
|
301
|
+
loading="lazy"
|
|
302
|
+
className="aspect-square w-full rounded-lg object-cover"
|
|
303
|
+
/>
|
|
304
|
+
) : null}
|
|
305
|
+
<StreamText field={staticField(product.name, "static")} as="h3" className="mt-3 font-medium" />
|
|
306
|
+
{product.description ? (
|
|
307
|
+
<StreamText
|
|
308
|
+
field={staticField(product.description, contentSource)}
|
|
309
|
+
as="p"
|
|
310
|
+
className="mt-1 text-sm opacity-75"
|
|
311
|
+
/>
|
|
312
|
+
) : null}
|
|
313
|
+
{product.price != null ? <p className="mt-2 font-semibold">{product.price}</p> : null}
|
|
314
|
+
</article>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
`;
|
|
318
|
+
const PROVIDER_TSX = `"use client";
|
|
319
|
+
|
|
320
|
+
import { SparkletreeProvider } from "@sparkletree/react";
|
|
321
|
+
import type { ReactNode } from "react";
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Wrap your app once.
|
|
325
|
+
*
|
|
326
|
+
* One provider per page view. It holds the org, the API base and the context
|
|
327
|
+
* snapshot, so every island on the page is adapted to the same moment rather
|
|
328
|
+
* than to two separate reads of the clock.
|
|
329
|
+
*
|
|
330
|
+
* The env checks below throw at module load, on purpose: a missing variable
|
|
331
|
+
* should fail your build or your first render with its own name in the error,
|
|
332
|
+
* not ship as a provider quietly pointed at \`undefined\`. The environment is
|
|
333
|
+
* read through globalThis because not every bundler (Vite, browser) defines
|
|
334
|
+
* \`process\` — there, the errors below tell you to wire the values yourself.
|
|
335
|
+
*/
|
|
336
|
+
const runtimeProcess = (globalThis as { process?: { env?: Record<string, string | undefined> } })
|
|
337
|
+
.process;
|
|
338
|
+
const env: Record<string, string | undefined> = runtimeProcess?.env ?? {};
|
|
339
|
+
const envApiBase = env.NEXT_PUBLIC_SPARKLETREE_API_BASE;
|
|
340
|
+
const envOrganizationId = env.NEXT_PUBLIC_SPARKLETREE_ORG_ID;
|
|
341
|
+
|
|
342
|
+
if (!envApiBase) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
runtimeProcess
|
|
345
|
+
? "SparkleTree: Set NEXT_PUBLIC_SPARKLETREE_API_BASE (for example in .env.local) " +
|
|
346
|
+
"to your SparkleTree API origin."
|
|
347
|
+
: "SparkleTree: this environment has no process global (Vite, browser, ...). " +
|
|
348
|
+
"Expose your SparkleTree API origin to the bundle (for example via import.meta.env) " +
|
|
349
|
+
"and assign it to apiBase below.",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
if (!envOrganizationId) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
runtimeProcess
|
|
355
|
+
? "SparkleTree: Set NEXT_PUBLIC_SPARKLETREE_ORG_ID (for example in .env.local) " +
|
|
356
|
+
"to your SparkleTree organization id."
|
|
357
|
+
: "SparkleTree: this environment has no process global (Vite, browser, ...). " +
|
|
358
|
+
"Expose your SparkleTree organization id to the bundle (for example via import.meta.env) " +
|
|
359
|
+
"and assign it to organizationId below.",
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const apiBase: string = envApiBase;
|
|
364
|
+
const organizationId: string = envOrganizationId;
|
|
365
|
+
|
|
366
|
+
export function SparkleTree({ children }: { children: ReactNode }) {
|
|
367
|
+
return (
|
|
368
|
+
<SparkletreeProvider apiBase={apiBase} organizationId={organizationId}>
|
|
369
|
+
{children}
|
|
370
|
+
</SparkletreeProvider>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
`;
|
|
374
|
+
export const REGISTRY = [
|
|
375
|
+
{
|
|
376
|
+
name: "provider",
|
|
377
|
+
description: "The page-level provider. Every other component needs it.",
|
|
378
|
+
files: [{ path: "sparkletree/provider.tsx", content: PROVIDER_TSX }],
|
|
379
|
+
dependencies: ["@sparkletree/react", "@sparkletree/core"],
|
|
380
|
+
registryDependencies: [],
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: "hero",
|
|
384
|
+
description: "Streaming hero: greeting, headline, body, adaptive art.",
|
|
385
|
+
files: [{ path: "sparkletree/hero.tsx", content: HERO_TSX }],
|
|
386
|
+
dependencies: ["@sparkletree/react", "@sparkletree/core"],
|
|
387
|
+
registryDependencies: ["provider"],
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: "cta",
|
|
391
|
+
description: "Streaming call to action with a stable hit area.",
|
|
392
|
+
files: [{ path: "sparkletree/cta.tsx", content: CTA_TSX }],
|
|
393
|
+
dependencies: ["@sparkletree/react", "@sparkletree/core"],
|
|
394
|
+
registryDependencies: ["provider"],
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: "copy-card",
|
|
398
|
+
description: "Content-fed copy card.",
|
|
399
|
+
files: [{ path: "sparkletree/copy-card.tsx", content: COPY_CARD_TSX }],
|
|
400
|
+
dependencies: ["@sparkletree/react"],
|
|
401
|
+
registryDependencies: ["provider"],
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: "product-card",
|
|
405
|
+
description: "Content-fed product card.",
|
|
406
|
+
files: [{ path: "sparkletree/product-card.tsx", content: PRODUCT_CARD_TSX }],
|
|
407
|
+
dependencies: ["@sparkletree/react"],
|
|
408
|
+
registryDependencies: ["provider"],
|
|
409
|
+
},
|
|
410
|
+
];
|
|
411
|
+
export function findComponent(name) {
|
|
412
|
+
return REGISTRY.find((component) => component.name === name);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Resolve a component plus everything it needs, dependencies first.
|
|
416
|
+
*
|
|
417
|
+
* Depth-first with a visited set, so a diamond in the graph copies each file
|
|
418
|
+
* once and a cycle terminates rather than recursing until the stack gives out.
|
|
419
|
+
*/
|
|
420
|
+
export function resolveWithDependencies(names) {
|
|
421
|
+
const resolved = [];
|
|
422
|
+
const seen = new Set();
|
|
423
|
+
const visit = (name) => {
|
|
424
|
+
if (seen.has(name))
|
|
425
|
+
return;
|
|
426
|
+
seen.add(name);
|
|
427
|
+
const component = findComponent(name);
|
|
428
|
+
if (!component)
|
|
429
|
+
throw new Error(`Unknown component "${name}"`);
|
|
430
|
+
for (const dependency of component.registryDependencies)
|
|
431
|
+
visit(dependency);
|
|
432
|
+
resolved.push(component);
|
|
433
|
+
};
|
|
434
|
+
for (const name of names)
|
|
435
|
+
visit(name);
|
|
436
|
+
return resolved;
|
|
437
|
+
}
|
|
438
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sparkletree/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "sparkletree — add SparkleTree components to your project.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sparkletree": "./dist/bin.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^20.19.11",
|
|
25
|
+
"typescript": "^5.9.2",
|
|
26
|
+
"vitest": "^3.2.4"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://gitlab.com/sparkletree/sparkletree-sdk.git",
|
|
34
|
+
"directory": "cli"
|
|
35
|
+
},
|
|
36
|
+
"author": "SparkleTree",
|
|
37
|
+
"homepage": "https://gitlab.com/sparkletree/sparkletree-sdk/-/blob/main/README.md",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://gitlab.com/sparkletree/sparkletree-sdk/-/issues"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"sparkletree",
|
|
43
|
+
"cli",
|
|
44
|
+
"components",
|
|
45
|
+
"scaffold",
|
|
46
|
+
"react"
|
|
47
|
+
],
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc --project tsconfig.json",
|
|
53
|
+
"clean": "rm -rf dist",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"typecheck": "tsc --project tsconfig.json --noEmit"
|
|
56
|
+
}
|
|
57
|
+
}
|