@gigamusic/links 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/README.md +34 -0
- package/dist/cli.js +191 -0
- package/dist/cli.js.map +1 -0
- package/package.json +76 -0
- package/prisma/_typegen.prisma +29 -0
- package/prisma/link-page.prisma +54 -0
- package/src/api/auth.ts +34 -0
- package/src/api/handlers.ts +267 -0
- package/src/api/validation.ts +15 -0
- package/src/cli/index.ts +95 -0
- package/src/cli/paths.ts +11 -0
- package/src/cli/sync.ts +169 -0
- package/src/client.ts +10 -0
- package/src/components/LinkButton.tsx +20 -0
- package/src/components/LinkPageHeader.tsx +35 -0
- package/src/components/LinkPageView.tsx +64 -0
- package/src/components/admin/DeleteLinkPageButton.tsx +52 -0
- package/src/components/admin/LinkPageForm.tsx +490 -0
- package/src/components/admin/NewLinkPageForm.tsx +152 -0
- package/src/components/url-helpers.ts +18 -0
- package/src/index.ts +80 -0
- package/src/pages/AdminEditLinkPagePage.tsx +57 -0
- package/src/pages/AdminLinkPagesIndexPage.tsx +107 -0
- package/src/pages/AdminNewLinkPagePage.tsx +26 -0
- package/src/pages/PublicLinkPage.tsx +117 -0
- package/src/platforms/detect.ts +51 -0
- package/src/platforms/icon.tsx +108 -0
- package/src/queries/link-pages.ts +175 -0
- package/src/server.ts +22 -0
- package/src/slots-context.tsx +46 -0
- package/src/slots.ts +36 -0
- package/src/types.ts +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @gigamusic/links
|
|
2
|
+
|
|
3
|
+
Linktree-style per-release link pages for `@gigamusic/*` artist sites. Ships its own Prisma fragment (`LinkPage`, `LinkPageItem`), a public `LinkPageView` component, admin page bodies + form, admin API handler factories, and platform-detection helpers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
createLinkPageQueries,
|
|
10
|
+
LinkPageView,
|
|
11
|
+
createPublicLinkPage,
|
|
12
|
+
createAdminLinkPagesHandlers,
|
|
13
|
+
detectLinkPlatform,
|
|
14
|
+
LinkPlatformIcon,
|
|
15
|
+
} from "@gigamusic/links";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The package is source-shipped — add `@gigamusic/links` to `transpilePackages` in your `next.config.ts` (the create-gigamusic-app scaffold does this automatically).
|
|
19
|
+
|
|
20
|
+
## Prisma fragment
|
|
21
|
+
|
|
22
|
+
Run `gigamusic-links sync` to copy the `link-page.prisma` fragment into `prisma/schema/20-link-page.prisma`. Run after every `@gigamusic/links` upgrade; `gigamusic-links doctor` checks for staleness.
|
|
23
|
+
|
|
24
|
+
The fragment references the `Release` model owned by `@gigamusic/db`. Prisma requires both sides of a relation to be declared, so consumers must add the inverse on their copy of `Release`:
|
|
25
|
+
|
|
26
|
+
```prisma
|
|
27
|
+
// prisma/schema/99-app.prisma (or wherever your Release lives)
|
|
28
|
+
model Release {
|
|
29
|
+
// ...existing fields...
|
|
30
|
+
linkPages LinkPage[]
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If your `Release` is shipped verbatim from `@gigamusic/db`'s `10-release.prisma`, add a thin override file at `99-release-link-pages.prisma` that re-declares the model with the extra field, or extend the upstream fragment when re-syncing.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, mkdirSync, existsSync, statSync, writeFileSync, readdirSync } from 'fs';
|
|
3
|
+
import { dirname, resolve, join } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
function fragmentsDir(metaUrl) {
|
|
7
|
+
const here = dirname(fileURLToPath(metaUrl));
|
|
8
|
+
return resolve(here, "..", "prisma");
|
|
9
|
+
}
|
|
10
|
+
var SYNC_PREFIX = "20-";
|
|
11
|
+
var VERSION_RE = /AUTOGENERATED by @gigamusic\/links@([^\s]+) on (\d{4}-\d{2}-\d{2})/;
|
|
12
|
+
function listFragmentFiles(sourceDir) {
|
|
13
|
+
return readdirSync(sourceDir).filter((f) => f.endsWith(".prisma") && !f.startsWith("_")).sort();
|
|
14
|
+
}
|
|
15
|
+
function buildHeader(version, today) {
|
|
16
|
+
return `// AUTOGENERATED by @gigamusic/links@${version} on ${today} \u2014 do not edit; re-run \`pnpm gigamusic-links sync\`
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
19
|
+
function readExistingVersion(filePath) {
|
|
20
|
+
if (!existsSync(filePath)) return null;
|
|
21
|
+
const head = readFileSync(filePath, "utf8").split("\n", 2).join("\n");
|
|
22
|
+
const match = VERSION_RE.exec(head);
|
|
23
|
+
return match?.[1] ?? null;
|
|
24
|
+
}
|
|
25
|
+
function compareVersions(a, b) {
|
|
26
|
+
if (a === b) return 0;
|
|
27
|
+
const aParts = a.split(/[.-]/);
|
|
28
|
+
const bParts = b.split(/[.-]/);
|
|
29
|
+
const len = Math.max(aParts.length, bParts.length);
|
|
30
|
+
for (let i = 0; i < len; i++) {
|
|
31
|
+
const ai = parseInt(aParts[i] ?? "0", 10);
|
|
32
|
+
const bi = parseInt(bParts[i] ?? "0", 10);
|
|
33
|
+
if (Number.isNaN(ai) || Number.isNaN(bi)) {
|
|
34
|
+
const as = aParts[i] ?? "";
|
|
35
|
+
const bs = bParts[i] ?? "";
|
|
36
|
+
if (as === bs) continue;
|
|
37
|
+
return as < bs ? -1 : 1;
|
|
38
|
+
}
|
|
39
|
+
if (ai !== bi) return ai < bi ? -1 : 1;
|
|
40
|
+
}
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
function syncFragments(opts, deps) {
|
|
44
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
45
|
+
const target = resolve(cwd, opts.target ?? join("prisma", "schema"));
|
|
46
|
+
const today = deps.today ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
47
|
+
const logger = deps.logger ?? console;
|
|
48
|
+
const header = buildHeader(deps.version, today);
|
|
49
|
+
const written = [];
|
|
50
|
+
const skipped = [];
|
|
51
|
+
if (!opts.dryRun) {
|
|
52
|
+
mkdirSync(target, { recursive: true });
|
|
53
|
+
} else if (!existsSync(target) || !statSync(target).isDirectory()) {
|
|
54
|
+
logger.info(`(dry-run) would create directory ${target}`);
|
|
55
|
+
}
|
|
56
|
+
for (const file of listFragmentFiles(deps.sourceDir)) {
|
|
57
|
+
const srcPath = join(deps.sourceDir, file);
|
|
58
|
+
const destPath = join(target, `${SYNC_PREFIX}${file}`);
|
|
59
|
+
const existingVersion = readExistingVersion(destPath);
|
|
60
|
+
if (existingVersion && compareVersions(existingVersion, deps.version) > 0) {
|
|
61
|
+
skipped.push({
|
|
62
|
+
file: destPath,
|
|
63
|
+
reason: `existing version ${existingVersion} is newer than installed ${deps.version}`
|
|
64
|
+
});
|
|
65
|
+
logger.warn(
|
|
66
|
+
`skip ${destPath} \u2014 existing v${existingVersion} is newer than installed v${deps.version}`
|
|
67
|
+
);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const body = readFileSync(srcPath, "utf8");
|
|
71
|
+
const contents = header + body;
|
|
72
|
+
if (opts.dryRun) {
|
|
73
|
+
logger.info(`(dry-run) would write ${destPath}`);
|
|
74
|
+
} else {
|
|
75
|
+
writeFileSync(destPath, contents, "utf8");
|
|
76
|
+
logger.info(`wrote ${destPath}`);
|
|
77
|
+
}
|
|
78
|
+
written.push(destPath);
|
|
79
|
+
}
|
|
80
|
+
return { written, skipped };
|
|
81
|
+
}
|
|
82
|
+
function doctorFragments(opts, deps) {
|
|
83
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
84
|
+
const target = resolve(cwd, opts.target ?? join("prisma", "schema"));
|
|
85
|
+
const stale = [];
|
|
86
|
+
const missing = [];
|
|
87
|
+
const logger = deps.logger ?? console;
|
|
88
|
+
for (const file of listFragmentFiles(deps.sourceDir)) {
|
|
89
|
+
const destPath = join(target, `${SYNC_PREFIX}${file}`);
|
|
90
|
+
const existingVersion = readExistingVersion(destPath);
|
|
91
|
+
if (existingVersion === null) {
|
|
92
|
+
missing.push(destPath);
|
|
93
|
+
logger.warn(`missing: ${destPath}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (compareVersions(existingVersion, deps.version) < 0) {
|
|
97
|
+
stale.push({
|
|
98
|
+
file: destPath,
|
|
99
|
+
installed: deps.version,
|
|
100
|
+
found: existingVersion
|
|
101
|
+
});
|
|
102
|
+
logger.warn(
|
|
103
|
+
`stale: ${destPath} is v${existingVersion}; installed @gigamusic/links is v${deps.version}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { ok: stale.length === 0 && missing.length === 0, stale, missing };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/cli/index.ts
|
|
111
|
+
function parseArgs(argv) {
|
|
112
|
+
const [command = "help", ...rest] = argv;
|
|
113
|
+
const parsed = {
|
|
114
|
+
command: ["sync", "doctor", "help", "version"].includes(command) ? command : "help",
|
|
115
|
+
dryRun: false
|
|
116
|
+
};
|
|
117
|
+
for (let i = 0; i < rest.length; i++) {
|
|
118
|
+
const arg = rest[i];
|
|
119
|
+
if (arg === "--dry-run") parsed.dryRun = true;
|
|
120
|
+
else if (arg === "--target") parsed.target = rest[++i];
|
|
121
|
+
else if (arg?.startsWith("--target=")) parsed.target = arg.slice("--target=".length);
|
|
122
|
+
}
|
|
123
|
+
return parsed;
|
|
124
|
+
}
|
|
125
|
+
function readPackageVersion(metaUrl) {
|
|
126
|
+
try {
|
|
127
|
+
const pkgPath = resolve(fragmentsDir(metaUrl), "..", "package.json");
|
|
128
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
129
|
+
return pkg.version ?? "0.0.0";
|
|
130
|
+
} catch {
|
|
131
|
+
return "0.0.0";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function printHelp() {
|
|
135
|
+
console.log(`gigamusic-links \u2014 Prisma fragment sync for @gigamusic/links
|
|
136
|
+
|
|
137
|
+
Usage:
|
|
138
|
+
gigamusic-links sync [--target <prismaSchemaFolder>] [--dry-run]
|
|
139
|
+
gigamusic-links doctor [--target <prismaSchemaFolder>]
|
|
140
|
+
gigamusic-links version
|
|
141
|
+
gigamusic-links help
|
|
142
|
+
|
|
143
|
+
Fragments are written with a "20-" prefix so they sort AFTER @gigamusic/db's
|
|
144
|
+
"10-" fragments and BEFORE consumer-owned "99-" files. Default --target is
|
|
145
|
+
./prisma/schema (Prisma 6 prismaSchemaFolder layout).
|
|
146
|
+
`);
|
|
147
|
+
}
|
|
148
|
+
async function main(argv, metaUrl) {
|
|
149
|
+
const args = parseArgs(argv);
|
|
150
|
+
const sourceDir = fragmentsDir(metaUrl);
|
|
151
|
+
const version = readPackageVersion(metaUrl);
|
|
152
|
+
if (args.command === "help") {
|
|
153
|
+
printHelp();
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
if (args.command === "version") {
|
|
157
|
+
console.log(version);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
if (args.command === "sync") {
|
|
161
|
+
syncFragments(
|
|
162
|
+
{ target: args.target, dryRun: args.dryRun },
|
|
163
|
+
{ sourceDir, version }
|
|
164
|
+
);
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
if (args.command === "doctor") {
|
|
168
|
+
const result = doctorFragments({ target: args.target }, { sourceDir, version });
|
|
169
|
+
if (!result.ok) {
|
|
170
|
+
console.error(
|
|
171
|
+
`gigamusic-links doctor: ${result.stale.length} stale, ${result.missing.length} missing`
|
|
172
|
+
);
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
console.log("gigamusic-links doctor: all fragments are up to date.");
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
printHelp();
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
main(process.argv.slice(2), import.meta.url).then(
|
|
182
|
+
(code) => process.exit(code),
|
|
183
|
+
(err) => {
|
|
184
|
+
console.error(err);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
export { main };
|
|
190
|
+
//# sourceMappingURL=cli.js.map
|
|
191
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/paths.ts","../src/cli/sync.ts","../src/cli/index.ts"],"names":["resolve","readFileSync"],"mappings":"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","file":"cli.js","sourcesContent":["import { fileURLToPath } from \"node:url\";\nimport { dirname, resolve } from \"node:path\";\n\n/**\n * Locate the shipped `prisma/` fragments directory. At runtime the CLI bundle\n * lives at `<pkg>/dist/cli.js`; the fragments live at `<pkg>/prisma/`.\n */\nexport function fragmentsDir(metaUrl: string): string {\n const here = dirname(fileURLToPath(metaUrl));\n return resolve(here, \"..\", \"prisma\");\n}\n","import {\n existsSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n statSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join, resolve } from \"node:path\";\n\nexport interface SyncOptions {\n /** Where the consumer's prisma schema folder lives. Defaults to `<cwd>/prisma/schema`. */\n target?: string;\n /** When true, log planned writes without touching the filesystem. */\n dryRun?: boolean;\n /** Override for testing — defaults to process.cwd(). */\n cwd?: string;\n}\n\nexport interface SyncResult {\n written: string[];\n skipped: { file: string; reason: string }[];\n}\n\nexport interface SyncDeps {\n sourceDir: string;\n version: string;\n today?: string;\n logger?: { info: (msg: string) => void; warn: (msg: string) => void };\n}\n\n/**\n * Sync prefix — `20-` orders after `10-` (gigamusic/db) and before `99-`\n * (consumer-owned). Keeping the prefix shared with @gigamusic/db's sort\n * convention so consumers don't need to learn a new rule.\n */\nconst SYNC_PREFIX = \"20-\";\n\nconst VERSION_RE = /AUTOGENERATED by @gigamusic\\/links@([^\\s]+) on (\\d{4}-\\d{2}-\\d{2})/;\n\nexport function listFragmentFiles(sourceDir: string): string[] {\n return readdirSync(sourceDir)\n .filter((f) => f.endsWith(\".prisma\") && !f.startsWith(\"_\"))\n .sort();\n}\n\nfunction buildHeader(version: string, today: string): string {\n return `// AUTOGENERATED by @gigamusic/links@${version} on ${today} — do not edit; re-run \\`pnpm gigamusic-links sync\\`\\n`;\n}\n\nfunction readExistingVersion(filePath: string): string | null {\n if (!existsSync(filePath)) return null;\n const head = readFileSync(filePath, \"utf8\").split(\"\\n\", 2).join(\"\\n\");\n const match = VERSION_RE.exec(head);\n return match?.[1] ?? null;\n}\n\n/**\n * Semver-ish comparison — accepts `x.y.z` or pre-release suffixes. Returns\n * 1/0/-1 like `Array.prototype.sort`. Workspace `workspace:*` and similar\n * non-numeric strings compare equal so we never refuse to overwrite during\n * local dev.\n */\nexport function compareVersions(a: string, b: string): number {\n if (a === b) return 0;\n const aParts = a.split(/[.-]/);\n const bParts = b.split(/[.-]/);\n const len = Math.max(aParts.length, bParts.length);\n for (let i = 0; i < len; i++) {\n const ai = parseInt(aParts[i] ?? \"0\", 10);\n const bi = parseInt(bParts[i] ?? \"0\", 10);\n if (Number.isNaN(ai) || Number.isNaN(bi)) {\n const as = aParts[i] ?? \"\";\n const bs = bParts[i] ?? \"\";\n if (as === bs) continue;\n return as < bs ? -1 : 1;\n }\n if (ai !== bi) return ai < bi ? -1 : 1;\n }\n return 0;\n}\n\nexport function syncFragments(opts: SyncOptions, deps: SyncDeps): SyncResult {\n const cwd = opts.cwd ?? process.cwd();\n const target = resolve(cwd, opts.target ?? join(\"prisma\", \"schema\"));\n const today = deps.today ?? new Date().toISOString().slice(0, 10);\n const logger = deps.logger ?? console;\n const header = buildHeader(deps.version, today);\n\n const written: string[] = [];\n const skipped: { file: string; reason: string }[] = [];\n\n if (!opts.dryRun) {\n mkdirSync(target, { recursive: true });\n } else if (!existsSync(target) || !statSync(target).isDirectory()) {\n logger.info(`(dry-run) would create directory ${target}`);\n }\n\n for (const file of listFragmentFiles(deps.sourceDir)) {\n const srcPath = join(deps.sourceDir, file);\n const destPath = join(target, `${SYNC_PREFIX}${file}`);\n const existingVersion = readExistingVersion(destPath);\n\n if (existingVersion && compareVersions(existingVersion, deps.version) > 0) {\n skipped.push({\n file: destPath,\n reason: `existing version ${existingVersion} is newer than installed ${deps.version}`,\n });\n logger.warn(\n `skip ${destPath} — existing v${existingVersion} is newer than installed v${deps.version}`,\n );\n continue;\n }\n\n const body = readFileSync(srcPath, \"utf8\");\n const contents = header + body;\n\n if (opts.dryRun) {\n logger.info(`(dry-run) would write ${destPath}`);\n } else {\n writeFileSync(destPath, contents, \"utf8\");\n logger.info(`wrote ${destPath}`);\n }\n written.push(destPath);\n }\n\n return { written, skipped };\n}\n\nexport interface DoctorOptions {\n target?: string;\n cwd?: string;\n}\n\nexport interface DoctorResult {\n ok: boolean;\n stale: { file: string; installed: string; found: string }[];\n missing: string[];\n}\n\nexport function doctorFragments(opts: DoctorOptions, deps: SyncDeps): DoctorResult {\n const cwd = opts.cwd ?? process.cwd();\n const target = resolve(cwd, opts.target ?? join(\"prisma\", \"schema\"));\n const stale: { file: string; installed: string; found: string }[] = [];\n const missing: string[] = [];\n const logger = deps.logger ?? console;\n\n for (const file of listFragmentFiles(deps.sourceDir)) {\n const destPath = join(target, `${SYNC_PREFIX}${file}`);\n const existingVersion = readExistingVersion(destPath);\n if (existingVersion === null) {\n missing.push(destPath);\n logger.warn(`missing: ${destPath}`);\n continue;\n }\n if (compareVersions(existingVersion, deps.version) < 0) {\n stale.push({\n file: destPath,\n installed: deps.version,\n found: existingVersion,\n });\n logger.warn(\n `stale: ${destPath} is v${existingVersion}; installed @gigamusic/links is v${deps.version}`,\n );\n }\n }\n\n return { ok: stale.length === 0 && missing.length === 0, stale, missing };\n}\n","import { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fragmentsDir } from \"./paths\";\nimport { doctorFragments, syncFragments } from \"./sync\";\n\ninterface ParsedArgs {\n command: \"sync\" | \"doctor\" | \"help\" | \"version\";\n target?: string;\n dryRun: boolean;\n}\n\nfunction parseArgs(argv: string[]): ParsedArgs {\n const [command = \"help\", ...rest] = argv;\n const parsed: ParsedArgs = {\n command: ([\"sync\", \"doctor\", \"help\", \"version\"].includes(command)\n ? command\n : \"help\") as ParsedArgs[\"command\"],\n dryRun: false,\n };\n for (let i = 0; i < rest.length; i++) {\n const arg = rest[i];\n if (arg === \"--dry-run\") parsed.dryRun = true;\n else if (arg === \"--target\") parsed.target = rest[++i];\n else if (arg?.startsWith(\"--target=\")) parsed.target = arg.slice(\"--target=\".length);\n }\n return parsed;\n}\n\nfunction readPackageVersion(metaUrl: string): string {\n try {\n const pkgPath = resolve(fragmentsDir(metaUrl), \"..\", \"package.json\");\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf8\")) as { version?: string };\n return pkg.version ?? \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\nfunction printHelp() {\n console.log(`gigamusic-links — Prisma fragment sync for @gigamusic/links\n\nUsage:\n gigamusic-links sync [--target <prismaSchemaFolder>] [--dry-run]\n gigamusic-links doctor [--target <prismaSchemaFolder>]\n gigamusic-links version\n gigamusic-links help\n\nFragments are written with a \"20-\" prefix so they sort AFTER @gigamusic/db's\n\"10-\" fragments and BEFORE consumer-owned \"99-\" files. Default --target is\n./prisma/schema (Prisma 6 prismaSchemaFolder layout).\n`);\n}\n\nexport async function main(argv: string[], metaUrl: string): Promise<number> {\n const args = parseArgs(argv);\n const sourceDir = fragmentsDir(metaUrl);\n const version = readPackageVersion(metaUrl);\n\n if (args.command === \"help\") {\n printHelp();\n return 0;\n }\n if (args.command === \"version\") {\n console.log(version);\n return 0;\n }\n if (args.command === \"sync\") {\n syncFragments(\n { target: args.target, dryRun: args.dryRun },\n { sourceDir, version },\n );\n return 0;\n }\n if (args.command === \"doctor\") {\n const result = doctorFragments({ target: args.target }, { sourceDir, version });\n if (!result.ok) {\n console.error(\n `gigamusic-links doctor: ${result.stale.length} stale, ${result.missing.length} missing`,\n );\n return 1;\n }\n console.log(\"gigamusic-links doctor: all fragments are up to date.\");\n return 0;\n }\n printHelp();\n return 1;\n}\n\nmain(process.argv.slice(2), import.meta.url).then(\n (code) => process.exit(code),\n (err) => {\n console.error(err);\n process.exit(1);\n },\n);\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gigamusic/links",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Editable Linktree-style link pages for gigamusic artist sites. Ships a Prisma fragment, public + admin React components, admin API handler factories, and platform-detection helpers.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/gigamesh/gigamusic.git",
|
|
9
|
+
"directory": "packages/links"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./src/index.ts",
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./src/index.ts",
|
|
17
|
+
"default": "./src/index.ts"
|
|
18
|
+
},
|
|
19
|
+
"./client": {
|
|
20
|
+
"types": "./src/client.ts",
|
|
21
|
+
"default": "./src/client.ts"
|
|
22
|
+
},
|
|
23
|
+
"./server": {
|
|
24
|
+
"types": "./src/server.ts",
|
|
25
|
+
"default": "./src/server.ts"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"gigamusic-links": "./dist/cli.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"src",
|
|
33
|
+
"prisma",
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@gigamusic/config": "0.1.0",
|
|
42
|
+
"@gigamusic/core": "0.1.0",
|
|
43
|
+
"@gigamusic/db": "0.1.0",
|
|
44
|
+
"@gigamusic/ui": "0.1.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@prisma/client": ">=6",
|
|
48
|
+
"next": ">=15",
|
|
49
|
+
"react": ">=18 <20",
|
|
50
|
+
"react-dom": ">=18 <20"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@prisma/client": "^6.1.0",
|
|
54
|
+
"@testing-library/dom": "^10.4.0",
|
|
55
|
+
"@testing-library/react": "^16.1.0",
|
|
56
|
+
"@types/node": "^22.10.5",
|
|
57
|
+
"@types/react": "^19.0.0",
|
|
58
|
+
"@types/react-dom": "^19.0.0",
|
|
59
|
+
"jsdom": "^25.0.1",
|
|
60
|
+
"next": "^16.0.0",
|
|
61
|
+
"react": "^19.0.0",
|
|
62
|
+
"react-dom": "^19.0.0",
|
|
63
|
+
"tsup": "^8.3.5",
|
|
64
|
+
"typescript": "^5.7.3",
|
|
65
|
+
"vitest": "^2.1.8"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "pnpm run prisma:generate && tsup",
|
|
69
|
+
"dev": "tsup --watch",
|
|
70
|
+
"lint": "eslint src",
|
|
71
|
+
"test": "pnpm run prisma:generate && vitest run",
|
|
72
|
+
"typecheck": "pnpm run prisma:generate && tsc --noEmit",
|
|
73
|
+
"prisma:generate": "prisma generate --schema=./prisma",
|
|
74
|
+
"clean": "rm -rf dist .turbo *.tsbuildinfo"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Internal — exists ONLY so `prisma generate --schema=./prisma` in THIS
|
|
2
|
+
// package can produce a PrismaClient with LinkPage + LinkPageItem on the
|
|
3
|
+
// delegate. The `gigamusic-links sync` CLI skips files starting with `_`,
|
|
4
|
+
// so this is never copied into a consumer app. The consumer's merged
|
|
5
|
+
// schema gets `Release` from `@gigamusic/db`; here we declare a minimal
|
|
6
|
+
// stub so the link-page.prisma relation compiles in isolation.
|
|
7
|
+
|
|
8
|
+
datasource db {
|
|
9
|
+
provider = "postgresql"
|
|
10
|
+
url = env("DATABASE_URL")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
generator client {
|
|
14
|
+
provider = "prisma-client-js"
|
|
15
|
+
binaryTargets = ["native"]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Minimal stub of @gigamusic/db's `Release` model — only the fields this
|
|
19
|
+
/// package's queries select. The consumer's merged schema redefines `Release`
|
|
20
|
+
/// with all its fields; this stub only needs to satisfy the link-page relation
|
|
21
|
+
/// and the columns we read.
|
|
22
|
+
model Release {
|
|
23
|
+
id Int @id @default(autoincrement())
|
|
24
|
+
name String
|
|
25
|
+
slug String @unique
|
|
26
|
+
coverImageUrl String?
|
|
27
|
+
isPublished Boolean @default(false)
|
|
28
|
+
linkPages LinkPage[]
|
|
29
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// gigamusic link-page schema fragment
|
|
2
|
+
//
|
|
3
|
+
// Shipped by `@gigamusic/links` and copied into a consumer app's
|
|
4
|
+
// `prisma/schema/` directory by `gigamusic-links sync` with a `20-` prefix
|
|
5
|
+
// (after `10-` fragments from @gigamusic/db, before consumer-owned `99-`).
|
|
6
|
+
//
|
|
7
|
+
// `LinkPage` references the `Release` model from @gigamusic/db via the
|
|
8
|
+
// `releaseId` scalar foreign key. Prisma requires *both* sides of every
|
|
9
|
+
// relation to be declared, so for the `release Release?` relation to compile
|
|
10
|
+
// the consumer must extend the upstream `Release` model with:
|
|
11
|
+
//
|
|
12
|
+
// model Release {
|
|
13
|
+
// // ...existing fields...
|
|
14
|
+
// linkPages LinkPage[]
|
|
15
|
+
// }
|
|
16
|
+
//
|
|
17
|
+
// The demo + create-gigamusic-app scaffolding wire this up automatically.
|
|
18
|
+
//
|
|
19
|
+
// Reserved model names: LinkPage, LinkPageItem
|
|
20
|
+
// Reserved table names: link_pages, link_page_items
|
|
21
|
+
|
|
22
|
+
model LinkPage {
|
|
23
|
+
id Int @id @default(autoincrement())
|
|
24
|
+
slug String @unique
|
|
25
|
+
title String
|
|
26
|
+
description String?
|
|
27
|
+
coverImageUrl String?
|
|
28
|
+
releaseId Int?
|
|
29
|
+
isPublished Boolean @default(true)
|
|
30
|
+
createdAt DateTime @default(now())
|
|
31
|
+
updatedAt DateTime @updatedAt
|
|
32
|
+
|
|
33
|
+
release Release? @relation(fields: [releaseId], references: [id], onDelete: SetNull)
|
|
34
|
+
items LinkPageItem[]
|
|
35
|
+
|
|
36
|
+
@@index([releaseId])
|
|
37
|
+
@@map("link_pages")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
model LinkPageItem {
|
|
41
|
+
id Int @id @default(autoincrement())
|
|
42
|
+
pageId Int
|
|
43
|
+
title String
|
|
44
|
+
url String
|
|
45
|
+
position Int @default(0)
|
|
46
|
+
isVisible Boolean @default(true)
|
|
47
|
+
createdAt DateTime @default(now())
|
|
48
|
+
updatedAt DateTime @updatedAt
|
|
49
|
+
|
|
50
|
+
page LinkPage @relation(fields: [pageId], references: [id], onDelete: Cascade)
|
|
51
|
+
|
|
52
|
+
@@index([pageId])
|
|
53
|
+
@@map("link_page_items")
|
|
54
|
+
}
|
package/src/api/auth.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { verifyAdminSessionToken } from "@gigamusic/core";
|
|
2
|
+
|
|
3
|
+
const ADMIN_SESSION_COOKIE = "admin_session";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Stateless cookie verification for route handlers. Mirrors
|
|
7
|
+
* `@gigamusic/admin`'s `verifyAdminSession` so consumers can wire both
|
|
8
|
+
* packages to the same session secret without coupling them at the package
|
|
9
|
+
* level.
|
|
10
|
+
*/
|
|
11
|
+
export async function verifyAdminSession(
|
|
12
|
+
req: Request,
|
|
13
|
+
adminSessionSecret: string,
|
|
14
|
+
): Promise<boolean> {
|
|
15
|
+
const header = req.headers.get("cookie");
|
|
16
|
+
if (!header) return false;
|
|
17
|
+
const token = parseCookieHeader(header, ADMIN_SESSION_COOKIE);
|
|
18
|
+
if (!token) return false;
|
|
19
|
+
try {
|
|
20
|
+
await verifyAdminSessionToken(token, adminSessionSecret);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseCookieHeader(header: string, name: string): string | null {
|
|
28
|
+
for (const raw of header.split(";")) {
|
|
29
|
+
const trimmed = raw.trim();
|
|
30
|
+
if (!trimmed.startsWith(`${name}=`)) continue;
|
|
31
|
+
return decodeURIComponent(trimmed.slice(name.length + 1));
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|