tackl 2.0.0 → 2.4.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 +7 -3
- package/bin/tackl.js +126 -36
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -9,13 +9,17 @@ mkdir my-app && cd my-app # or clone your empty repo and cd in
|
|
|
9
9
|
bunx tackl # or: bun create tackl / npm create tackl@latest
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
You'll be asked which CMS to use — **DatoCMS**, **Sanity**, or **None** — and the scaffold is pruned to match: only the chosen adapter, its dependencies, its docs, and its `.env.example` block survive.
|
|
12
|
+
You'll be asked which CMS to use — **DatoCMS**, **Sanity**, or **None** — and the scaffold is pruned to match: only the chosen adapter, its dependencies, its docs, and its `.env.example` block survive. Draft preview ships with both CMS choices (the `app/api/draft-mode/` routes resolve through the adapter seam). **None** keeps the same shape as the others — the `@cms` seam stays, rewired to a stub adapter whose `fetchContent` returns `null` — so wiring up a CMS later never touches app code; only the draft-mode routes and CMS files are removed.
|
|
13
|
+
|
|
14
|
+
Choosing **Sanity** sets the whole thing up: an embedded Studio at `/studio`, example schemas, and draft-mode preview routes ship in the scaffold, and the CLI offers to run `sanity init` for you — logging in, creating/linking a project, and writing your `.env`. Scaffold → `bun run dev` → `/studio` → start editing.
|
|
15
|
+
|
|
16
|
+
Design tokens live in `src/theme` and type styles in `src/css/tackl/_type.scss` — set them from your Figma file before you start building.
|
|
13
17
|
|
|
14
18
|
## What it does
|
|
15
19
|
|
|
16
20
|
1. Verifies the directory is empty (an existing `.git`, `.gitignore`, `README.md` or `LICENSE` is fine — your repo's `.git` and remote are kept)
|
|
17
|
-
2. Downloads the latest
|
|
18
|
-
3. Prunes repo-only cargo (the CLI itself, `.github`) and the unused CMS adapter
|
|
21
|
+
2. Downloads the latest GitHub release of the template (falls back to `main` if none can be resolved). The lookup uses `GITHUB_TOKEN` / `GH_TOKEN` when set, and otherwise falls back to github.com's non-rate-limited release redirect — so shared IPs (offices, CI) still get the release
|
|
22
|
+
3. Prunes repo-only cargo (the CLI itself, `.github` templates, internal `docs/plans`) and the unused CMS adapter
|
|
19
23
|
4. Sets your package name, initialises git if needed, and runs `bun install`
|
|
20
24
|
|
|
21
25
|
## Options
|
package/bin/tackl.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execSync } from 'child_process';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
import path from 'path';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
6
|
import pc from 'picocolors';
|
|
7
7
|
import prompts from 'prompts';
|
|
8
8
|
import * as tar from 'tar';
|
|
@@ -55,18 +55,44 @@ const dirIsScaffoldable = dir => {
|
|
|
55
55
|
// SECTION • Template ref resolution
|
|
56
56
|
// NOTE • Prefer the latest GitHub release so every scaffold is a tested,
|
|
57
57
|
// reproducible snapshot; fall back to main (with a warning) when no
|
|
58
|
-
// release
|
|
58
|
+
// release can be resolved. --ref overrides both.
|
|
59
|
+
// The REST API allows only 60 unauthenticated requests/hour per IP — shared
|
|
60
|
+
// IPs (offices, CI) hit that — so a GITHUB_TOKEN is used when present, and
|
|
61
|
+
// the github.com /releases/latest redirect (not rate-limited) is tried next.
|
|
62
|
+
const RELEASE_TAG_PATTERN = /\/releases\/tag\/([^/?#]+)/;
|
|
63
|
+
|
|
64
|
+
const latestReleaseFromApi = async () => {
|
|
65
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
66
|
+
const res = await fetch(`https://api.github.com/repos/${OWNER}/${REPO}/releases/latest`, {
|
|
67
|
+
headers: {
|
|
68
|
+
accept: 'application/vnd.github+json',
|
|
69
|
+
'user-agent': 'tackl-cli',
|
|
70
|
+
...(token && { authorization: `Bearer ${token}` }),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) return null;
|
|
74
|
+
const json = await res.json();
|
|
75
|
+
return json?.tag_name ?? null;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const latestReleaseFromRedirect = async () => {
|
|
79
|
+
const res = await fetch(`https://github.com/${OWNER}/${REPO}/releases/latest`, {
|
|
80
|
+
method: 'HEAD',
|
|
81
|
+
redirect: 'manual',
|
|
82
|
+
headers: { 'user-agent': 'tackl-cli' },
|
|
83
|
+
});
|
|
84
|
+
const match = res.headers.get('location')?.match(RELEASE_TAG_PATTERN);
|
|
85
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
86
|
+
};
|
|
87
|
+
|
|
59
88
|
const resolveRef = async explicitRef => {
|
|
60
89
|
if (explicitRef) return { ref: explicitRef, source: 'flag' };
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (json?.tag_name) return { ref: json.tag_name, source: 'release' };
|
|
68
|
-
}
|
|
69
|
-
} catch {}
|
|
90
|
+
for (const lookup of [latestReleaseFromApi, latestReleaseFromRedirect]) {
|
|
91
|
+
try {
|
|
92
|
+
const tag = await lookup();
|
|
93
|
+
if (tag) return { ref: tag, source: 'release' };
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
70
96
|
return { ref: FALLBACK_REF, source: 'fallback' };
|
|
71
97
|
};
|
|
72
98
|
|
|
@@ -104,7 +130,24 @@ const stripDeps = (root, deps) => {
|
|
|
104
130
|
delete json.dependencies?.[d];
|
|
105
131
|
delete json.devDependencies?.[d];
|
|
106
132
|
}
|
|
107
|
-
fs.writeFileSync(p, JSON.stringify(json, null, '\t')
|
|
133
|
+
fs.writeFileSync(p, `${JSON.stringify(json, null, '\t')}\n`);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// NOTE • Everything that exists solely for the embedded Sanity Studio.
|
|
137
|
+
// app/api/draft-mode is NOT here — the draft routes are CMS-agnostic
|
|
138
|
+
// (they resolve through @cms/draft) and only go when no CMS is chosen.
|
|
139
|
+
const SANITY_STUDIO_PATHS = ['app/(studio)', 'sanity.config.ts', 'sanity'];
|
|
140
|
+
const SANITY_DEPS = ['next-sanity', 'sanity', '@sanity/vision'];
|
|
141
|
+
|
|
142
|
+
// NOTE • The two files the CLI rewires to the chosen adapter
|
|
143
|
+
const ADAPTER_ENTRIES = ['src/cms/index.ts', 'src/cms/draft.ts'];
|
|
144
|
+
const rewireAdapter = (root, adapter) => {
|
|
145
|
+
for (const rel of ADAPTER_ENTRIES) {
|
|
146
|
+
const p = path.join(root, rel);
|
|
147
|
+
if (fs.existsSync(p)) {
|
|
148
|
+
fs.writeFileSync(p, fs.readFileSync(p, 'utf8').replaceAll(`'./dato`, `'./${adapter}`));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
108
151
|
};
|
|
109
152
|
|
|
110
153
|
const CMS = {
|
|
@@ -112,8 +155,10 @@ const CMS = {
|
|
|
112
155
|
title: 'DatoCMS',
|
|
113
156
|
prune: root => {
|
|
114
157
|
rmrf(root, 'src/cms/sanity');
|
|
158
|
+
rmrf(root, 'src/cms/none');
|
|
115
159
|
rmrf(root, 'docs/Sanity');
|
|
116
|
-
|
|
160
|
+
for (const p of SANITY_STUDIO_PATHS) rmrf(root, p);
|
|
161
|
+
stripDeps(root, SANITY_DEPS);
|
|
117
162
|
stripEnvBlock(root, '# CMS — Sanity');
|
|
118
163
|
},
|
|
119
164
|
},
|
|
@@ -121,32 +166,46 @@ const CMS = {
|
|
|
121
166
|
title: 'Sanity',
|
|
122
167
|
prune: root => {
|
|
123
168
|
rmrf(root, 'src/cms/dato');
|
|
169
|
+
rmrf(root, 'src/cms/none');
|
|
124
170
|
rmrf(root, 'docs/DatoCMS');
|
|
125
171
|
stripDeps(root, ['@datocms/cda-client', 'react-datocms', 'graphql', 'graphql-tag']);
|
|
126
172
|
stripEnvBlock(root, '# CMS — DatoCMS');
|
|
127
|
-
|
|
128
|
-
const entry = path.join(root, 'src/cms/index.ts');
|
|
129
|
-
if (fs.existsSync(entry)) {
|
|
130
|
-
fs.writeFileSync(entry, fs.readFileSync(entry, 'utf8').replace(`'./dato'`, `'./sanity'`));
|
|
131
|
-
}
|
|
173
|
+
rewireAdapter(root, 'sanity');
|
|
132
174
|
},
|
|
133
175
|
},
|
|
134
176
|
none: {
|
|
135
177
|
title: 'No CMS',
|
|
178
|
+
// NOTE • Same shape as the other choices — the '@cms' seam survives,
|
|
179
|
+
// rewired to the stub adapter (src/cms/none), so app code and the
|
|
180
|
+
// tsconfig alias stay valid and a CMS can be wired up later. Only the
|
|
181
|
+
// CMS-specific files go: adapters, draft-mode routes, studio, docs.
|
|
136
182
|
prune: root => {
|
|
137
|
-
rmrf(root, 'src/cms');
|
|
183
|
+
rmrf(root, 'src/cms/dato');
|
|
184
|
+
rmrf(root, 'src/cms/sanity');
|
|
185
|
+
rmrf(root, 'src/cms/draft.ts');
|
|
138
186
|
rmrf(root, 'docs/Sanity');
|
|
139
187
|
rmrf(root, 'docs/DatoCMS');
|
|
140
|
-
rmrf(root, '
|
|
141
|
-
|
|
188
|
+
rmrf(root, 'app/api/draft-mode');
|
|
189
|
+
for (const p of SANITY_STUDIO_PATHS) rmrf(root, p);
|
|
190
|
+
stripDeps(root, [...SANITY_DEPS, '@datocms/cda-client', 'react-datocms', 'graphql', 'graphql-tag']);
|
|
142
191
|
stripEnvBlock(root, '# CMS — Sanity');
|
|
143
192
|
stripEnvBlock(root, '# CMS — DatoCMS');
|
|
193
|
+
rewireAdapter(root, 'none');
|
|
144
194
|
},
|
|
145
195
|
},
|
|
146
196
|
};
|
|
147
197
|
|
|
148
|
-
// NOTE • Repo-only cargo that should never land in a scaffolded project
|
|
149
|
-
|
|
198
|
+
// NOTE • Repo-only cargo that should never land in a scaffolded project.
|
|
199
|
+
// .github is pruned file-by-file — the CI workflow (.github/workflows) ships
|
|
200
|
+
// with every scaffold so quality gates run from the first push.
|
|
201
|
+
const REPO_CARGO = [
|
|
202
|
+
'tackl',
|
|
203
|
+
'docs/plans',
|
|
204
|
+
'.github/FUNDING.yml',
|
|
205
|
+
'.github/ISSUE_TEMPLATE.md',
|
|
206
|
+
'.github/PULL_REQUEST_TEMPLATE.md',
|
|
207
|
+
'bun.lock',
|
|
208
|
+
];
|
|
150
209
|
|
|
151
210
|
async function main() {
|
|
152
211
|
const args = process.argv.slice(2);
|
|
@@ -182,7 +241,9 @@ ${pc.bold('Repository:')} https://github.com/${OWNER}/${REPO}
|
|
|
182
241
|
const targetDir = process.cwd();
|
|
183
242
|
|
|
184
243
|
if (!dirIsScaffoldable(targetDir)) {
|
|
185
|
-
log.error(
|
|
244
|
+
log.error(
|
|
245
|
+
'This directory is not empty. Run tackl inside an empty directory (a .git folder, .gitignore, README.md and LICENSE are fine).'
|
|
246
|
+
);
|
|
186
247
|
process.exit(1);
|
|
187
248
|
}
|
|
188
249
|
|
|
@@ -219,10 +280,11 @@ ${pc.bold('Repository:')} https://github.com/${OWNER}/${REPO}
|
|
|
219
280
|
|
|
220
281
|
const cms = cmsFlag || answers.cms;
|
|
221
282
|
const rawName = flagValue('--name') || path.basename(targetDir);
|
|
222
|
-
const projectName =
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
283
|
+
const projectName =
|
|
284
|
+
rawName
|
|
285
|
+
.toLowerCase()
|
|
286
|
+
.replace(/[^a-z0-9\-._]/g, '-')
|
|
287
|
+
.replace(/^[-.]+|[-.]+$/g, '') || 'tackl-app';
|
|
226
288
|
const doInstall = !args.includes('--no-install');
|
|
227
289
|
const doGit = !args.includes('--no-git');
|
|
228
290
|
const hadGit = fs.existsSync(path.join(targetDir, '.git'));
|
|
@@ -294,7 +356,7 @@ ${pc.bold('Repository:')} https://github.com/${OWNER}/${REPO}
|
|
|
294
356
|
json.version = '0.1.0';
|
|
295
357
|
delete json.repository;
|
|
296
358
|
delete json.author;
|
|
297
|
-
fs.writeFileSync(pkgPath, JSON.stringify(json, null, '\t')
|
|
359
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(json, null, '\t')}\n`);
|
|
298
360
|
log.ok('package.json updated.');
|
|
299
361
|
} catch (e) {
|
|
300
362
|
log.warn(`Could not update package.json: ${e.message}`);
|
|
@@ -330,11 +392,39 @@ ${pc.bold('Repository:')} https://github.com/${OWNER}/${REPO}
|
|
|
330
392
|
log.info('Skipped dependency install.');
|
|
331
393
|
}
|
|
332
394
|
|
|
333
|
-
|
|
395
|
+
// SECTION • Sanity project link
|
|
396
|
+
// NOTE • A Sanity scaffold isn't usable until a project id exists —
|
|
397
|
+
// sanity's own init logs in, creates/links a project and writes .env.
|
|
398
|
+
let sanityLinked = false;
|
|
399
|
+
if (cms === 'sanity' && process.stdout.isTTY) {
|
|
400
|
+
const { link } = await prompts({
|
|
401
|
+
type: 'confirm',
|
|
402
|
+
name: 'link',
|
|
403
|
+
message: 'Link a Sanity project now? (opens a browser login, writes .env)',
|
|
404
|
+
initial: true,
|
|
405
|
+
});
|
|
406
|
+
if (link) {
|
|
407
|
+
try {
|
|
408
|
+
execSync('bunx sanity@latest init --env .env', { cwd: targetDir, stdio: 'inherit' });
|
|
409
|
+
sanityLinked = true;
|
|
410
|
+
log.ok('Sanity project linked!');
|
|
411
|
+
} catch (e) {
|
|
412
|
+
log.warn(`sanity init did not complete: ${e.message}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
console.log(`\n${pc.bold(pc.green('Success!'))} Project scaffolded.\n`);
|
|
334
418
|
console.log(' Next steps:');
|
|
335
|
-
|
|
336
|
-
if (
|
|
337
|
-
|
|
419
|
+
let step = 1;
|
|
420
|
+
if (cms === 'sanity' && !sanityLinked) {
|
|
421
|
+
console.log(` ${step++}) bunx sanity init --env .env # create/link your Sanity project`);
|
|
422
|
+
} else if (cms !== 'sanity') {
|
|
423
|
+
console.log(` ${step++}) cp .env.example .env # then fill in your values`);
|
|
424
|
+
}
|
|
425
|
+
if (!doInstall) console.log(` ${step++}) bun install`);
|
|
426
|
+
console.log(` ${step++}) bun run dev # then set your tokens in src/theme`);
|
|
427
|
+
if (cms === 'sanity') console.log(` ${step}) open http://localhost:3000/studio to start editing content`);
|
|
338
428
|
}
|
|
339
429
|
|
|
340
430
|
main().catch(e => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tackl",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Scaffold a Tackl project into the current directory — Next.js,
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "Scaffold a Tackl project into the current directory — Next.js, SCSS CSS Modules, Waffl grid, DatoCMS/Sanity adapters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"tackl": "bin/tackl.js"
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"template",
|
|
18
18
|
"grid",
|
|
19
19
|
"css",
|
|
20
|
-
"
|
|
20
|
+
"scss",
|
|
21
|
+
"css-modules",
|
|
21
22
|
"typescript"
|
|
22
23
|
],
|
|
23
24
|
"author": "joebentaylor1995",
|
|
@@ -40,6 +41,6 @@
|
|
|
40
41
|
"dependencies": {
|
|
41
42
|
"picocolors": "^1.1.1",
|
|
42
43
|
"prompts": "^2.4.2",
|
|
43
|
-
"tar": "^7.5.
|
|
44
|
+
"tar": "^7.5.22"
|
|
44
45
|
}
|
|
45
46
|
}
|