tackl 2.2.0 → 2.5.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 +4 -4
- package/bin/tackl.js +76 -22
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -9,17 +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
13
|
|
|
14
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
15
|
|
|
16
|
-
|
|
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.
|
|
17
17
|
|
|
18
18
|
## What it does
|
|
19
19
|
|
|
20
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)
|
|
21
|
-
2. Downloads the latest
|
|
22
|
-
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
|
|
23
23
|
4. Sets your package name, initialises git if needed, and runs `bun install`
|
|
24
24
|
|
|
25
25
|
## Options
|
package/bin/tackl.js
CHANGED
|
@@ -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
|
|
|
@@ -107,15 +133,29 @@ const stripDeps = (root, deps) => {
|
|
|
107
133
|
fs.writeFileSync(p, `${JSON.stringify(json, null, '\t')}\n`);
|
|
108
134
|
};
|
|
109
135
|
|
|
110
|
-
// NOTE • Everything that exists solely for the embedded Sanity Studio
|
|
111
|
-
|
|
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'];
|
|
112
140
|
const SANITY_DEPS = ['next-sanity', 'sanity', '@sanity/vision'];
|
|
113
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
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
114
153
|
const CMS = {
|
|
115
154
|
dato: {
|
|
116
155
|
title: 'DatoCMS',
|
|
117
156
|
prune: root => {
|
|
118
157
|
rmrf(root, 'src/cms/sanity');
|
|
158
|
+
rmrf(root, 'src/cms/none');
|
|
119
159
|
rmrf(root, 'docs/Sanity');
|
|
120
160
|
for (const p of SANITY_STUDIO_PATHS) rmrf(root, p);
|
|
121
161
|
stripDeps(root, SANITY_DEPS);
|
|
@@ -126,33 +166,47 @@ const CMS = {
|
|
|
126
166
|
title: 'Sanity',
|
|
127
167
|
prune: root => {
|
|
128
168
|
rmrf(root, 'src/cms/dato');
|
|
169
|
+
rmrf(root, 'src/cms/none');
|
|
129
170
|
rmrf(root, 'docs/DatoCMS');
|
|
130
171
|
stripDeps(root, ['@datocms/cda-client', 'react-datocms', 'graphql', 'graphql-tag']);
|
|
131
172
|
stripEnvBlock(root, '# CMS — DatoCMS');
|
|
132
|
-
|
|
133
|
-
const entry = path.join(root, 'src/cms/index.ts');
|
|
134
|
-
if (fs.existsSync(entry)) {
|
|
135
|
-
fs.writeFileSync(entry, fs.readFileSync(entry, 'utf8').replace(`'./dato'`, `'./sanity'`));
|
|
136
|
-
}
|
|
173
|
+
rewireAdapter(root, 'sanity');
|
|
137
174
|
},
|
|
138
175
|
},
|
|
139
176
|
none: {
|
|
140
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.
|
|
141
182
|
prune: root => {
|
|
142
|
-
rmrf(root, 'src/cms');
|
|
183
|
+
rmrf(root, 'src/cms/dato');
|
|
184
|
+
rmrf(root, 'src/cms/sanity');
|
|
185
|
+
rmrf(root, 'src/cms/draft.ts');
|
|
143
186
|
rmrf(root, 'docs/Sanity');
|
|
144
187
|
rmrf(root, 'docs/DatoCMS');
|
|
145
|
-
rmrf(root, '
|
|
188
|
+
rmrf(root, 'app/api/draft-mode');
|
|
146
189
|
for (const p of SANITY_STUDIO_PATHS) rmrf(root, p);
|
|
147
190
|
stripDeps(root, [...SANITY_DEPS, '@datocms/cda-client', 'react-datocms', 'graphql', 'graphql-tag']);
|
|
148
191
|
stripEnvBlock(root, '# CMS — Sanity');
|
|
149
192
|
stripEnvBlock(root, '# CMS — DatoCMS');
|
|
193
|
+
rewireAdapter(root, 'none');
|
|
150
194
|
},
|
|
151
195
|
},
|
|
152
196
|
};
|
|
153
197
|
|
|
154
|
-
// NOTE • Repo-only cargo that should never land in a scaffolded project
|
|
155
|
-
|
|
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
|
+
'.github/workflows/release.yml',
|
|
208
|
+
'bun.lock',
|
|
209
|
+
];
|
|
156
210
|
|
|
157
211
|
async function main() {
|
|
158
212
|
const args = process.argv.slice(2);
|
|
@@ -370,7 +424,7 @@ ${pc.bold('Repository:')} https://github.com/${OWNER}/${REPO}
|
|
|
370
424
|
console.log(` ${step++}) cp .env.example .env # then fill in your values`);
|
|
371
425
|
}
|
|
372
426
|
if (!doInstall) console.log(` ${step++}) bun install`);
|
|
373
|
-
console.log(` ${step++}) bun run dev #
|
|
427
|
+
console.log(` ${step++}) bun run dev # then set your tokens in src/theme`);
|
|
374
428
|
if (cms === 'sanity') console.log(` ${step}) open http://localhost:3000/studio to start editing content`);
|
|
375
429
|
}
|
|
376
430
|
|
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.5.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",
|
|
@@ -27,11 +28,11 @@
|
|
|
27
28
|
},
|
|
28
29
|
"repository": {
|
|
29
30
|
"type": "git",
|
|
30
|
-
"url": "git+https://github.com/12-
|
|
31
|
+
"url": "git+https://github.com/12-Studio/Tackl.git"
|
|
31
32
|
},
|
|
32
|
-
"homepage": "https://github.com/12-
|
|
33
|
+
"homepage": "https://github.com/12-Studio/Tackl",
|
|
33
34
|
"bugs": {
|
|
34
|
-
"url": "https://github.com/12-
|
|
35
|
+
"url": "https://github.com/12-Studio/Tackl/issues"
|
|
35
36
|
},
|
|
36
37
|
"files": [
|
|
37
38
|
"bin/",
|
|
@@ -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
|
}
|