anhedral 0.4.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -265
- package/assets/anhedral-cli-init-technical.svg +322 -0
- package/assets/anhedral-cli-init.svg +257 -173
- package/dist/bin.js +28 -191
- package/dist/dependencies.d.ts +7 -57
- package/dist/dependencies.js +96 -308
- package/dist/index.d.ts +2 -7
- package/dist/index.js +1 -4
- package/dist/platforms/desktop.js +401 -0
- package/dist/platforms/extension.js +401 -0
- package/dist/platforms/mobile.js +232 -0
- package/dist/project.d.ts +0 -26
- package/dist/standard-products.d.ts +2 -0
- package/dist/standard-products.js +6 -0
- package/dist/standard.d.ts +18 -0
- package/dist/standard.js +595 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/application-stack-standard.md +700 -0
- package/package.json +29 -51
- package/CHANGELOG.md +0 -178
- package/CONTRIBUTING.md +0 -77
- package/SECURITY.md +0 -37
- package/SKILL.md +0 -339
- package/assets/images/svg/ably-symbol-color.svg +0 -8
- package/bin/anhedral.js +0 -2
- package/dist/architecture/contributions.d.ts +0 -28
- package/dist/architecture/contributions.js +0 -125
- package/dist/architecture/index.d.ts +0 -12
- package/dist/architecture/index.js +0 -6
- package/dist/architecture/manifest.d.ts +0 -53
- package/dist/architecture/manifest.js +0 -283
- package/dist/architecture/modules.d.ts +0 -41
- package/dist/architecture/modules.js +0 -187
- package/dist/architecture/plan.d.ts +0 -58
- package/dist/architecture/plan.js +0 -145
- package/dist/architecture/templates.d.ts +0 -10
- package/dist/architecture/templates.js +0 -23
- package/dist/cli.d.ts +0 -32
- package/dist/cli.js +0 -281
- package/dist/composers/root.d.ts +0 -6
- package/dist/composers/root.js +0 -35
- package/dist/print.d.ts +0 -8
- package/dist/print.js +0 -48
- package/dist/prompts.d.ts +0 -6
- package/dist/prompts.js +0 -41
- package/dist/scaffold.d.ts +0 -64
- package/dist/scaffold.js +0 -2102
- package/dist/template-source.d.ts +0 -20
- package/dist/template-source.js +0 -180
- package/dist/templates/api.d.ts +0 -2
- package/dist/templates/api.js +0 -3187
- package/dist/templates/assets-private-proxy.d.ts +0 -3
- package/dist/templates/assets-private-proxy.js +0 -277
- package/dist/templates/desktop.js +0 -792
- package/dist/templates/electron-updater.d.ts +0 -3
- package/dist/templates/electron-updater.js +0 -358
- package/dist/templates/extension.js +0 -856
- package/dist/templates/first-run.d.ts +0 -2
- package/dist/templates/first-run.js +0 -167
- package/dist/templates/mobile.js +0 -861
- package/dist/templates/shared.d.ts +0 -2
- package/dist/templates/shared.js +0 -577
- package/dist/templates/web.d.ts +0 -2
- package/dist/templates/web.js +0 -569
- package/dist/toolchain.d.ts +0 -3
- package/dist/toolchain.js +0 -8
- package/dist/ui.d.ts +0 -33
- package/dist/ui.js +0 -199
- package/dist/workspace-config.d.ts +0 -11
- package/dist/workspace-config.js +0 -372
- package/docs/NORTHSTAR.md +0 -114
- package/docs/conventions.md +0 -94
- package/docs/engineering-principles.md +0 -88
- package/docs/master-stack-map.md +0 -199
- package/docs/output-tree-contract.md +0 -493
- package/docs/references/manual-scaffolding.md +0 -421
- package/docs/references/provisioning.md +0 -577
- package/scripts/render-master-stack-map.mjs +0 -510
- package/templates/api-fastify/apps/api/tsconfig.json +0 -20
- package/templates/catalog.json +0 -23
- package/templates/db-drizzle/packages/db/tsconfig.json +0 -14
- package/templates/desktop-electron/apps/desktop/tsconfig.json +0 -37
- package/templates/extension-wxt/apps/extension/tsconfig.json +0 -41
- package/templates/mobile-expo/apps/mobile/expo-env.d.ts +0 -1
- package/templates/web-next/apps/web/next-env.d.ts +0 -6
- /package/dist/{templates → platforms}/desktop.d.ts +0 -0
- /package/dist/{templates → platforms}/extension.d.ts +0 -0
- /package/dist/{templates → platforms}/mobile.d.ts +0 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { writeFile } from '../util.js';
|
|
4
|
+
import { EXTENSION_DEPENDENCIES } from '../dependencies.js';
|
|
5
|
+
import { childPackageName, htmlText, jsString, markdownHeading } from '../render.js';
|
|
6
|
+
export async function scaffoldExtension(root, options) {
|
|
7
|
+
const { projectName, displayName } = options;
|
|
8
|
+
const dir = path.join(root, 'apps/extension');
|
|
9
|
+
writePackageJson(dir, projectName);
|
|
10
|
+
cleanWxtStarterFiles(dir);
|
|
11
|
+
writeWxtConfig(dir, displayName);
|
|
12
|
+
writeEnvExample(dir);
|
|
13
|
+
writePostcssConfig(dir);
|
|
14
|
+
writeTailwindConfig(dir);
|
|
15
|
+
writeShadcnConfig(dir);
|
|
16
|
+
writeReadme(dir, displayName);
|
|
17
|
+
writeCnUtil(dir);
|
|
18
|
+
writeButtonComponent(dir);
|
|
19
|
+
writeBackground(dir);
|
|
20
|
+
writeSidepanelEntry(dir);
|
|
21
|
+
writeSidepanelHtml(dir, displayName);
|
|
22
|
+
writeSidepanelApp(dir);
|
|
23
|
+
writeStyles(dir);
|
|
24
|
+
}
|
|
25
|
+
function cleanWxtStarterFiles(dir) {
|
|
26
|
+
for (const relativePath of [
|
|
27
|
+
'.git',
|
|
28
|
+
'entrypoints',
|
|
29
|
+
'assets/react.svg',
|
|
30
|
+
'public/wxt.svg',
|
|
31
|
+
]) {
|
|
32
|
+
rmSync(path.join(dir, relativePath), { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function writePackageJson(dir, projectName) {
|
|
36
|
+
writeFile(path.join(dir, 'package.json'), JSON.stringify({
|
|
37
|
+
name: childPackageName(projectName, 'chrome-ext'),
|
|
38
|
+
version: '0.1.0',
|
|
39
|
+
private: true,
|
|
40
|
+
type: 'module',
|
|
41
|
+
scripts: {
|
|
42
|
+
dev: 'wxt',
|
|
43
|
+
build: 'wxt build',
|
|
44
|
+
postinstall: 'wxt prepare',
|
|
45
|
+
zip: 'wxt zip',
|
|
46
|
+
typecheck: 'tsc --noEmit',
|
|
47
|
+
},
|
|
48
|
+
dependencies: EXTENSION_DEPENDENCIES.dependencies,
|
|
49
|
+
devDependencies: EXTENSION_DEPENDENCIES.devDependencies,
|
|
50
|
+
}, null, 2) + '\n');
|
|
51
|
+
}
|
|
52
|
+
function writeWxtConfig(dir, displayName) {
|
|
53
|
+
const nameLiteral = jsString(displayName);
|
|
54
|
+
const descriptionLiteral = jsString(`${displayName} Chrome Extension`);
|
|
55
|
+
const actionTitleLiteral = jsString(`Open ${displayName}`);
|
|
56
|
+
writeFile(path.join(dir, 'wxt.config.ts'), `import { defineConfig } from 'wxt';
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
export default defineConfig({
|
|
60
|
+
srcDir: 'src',
|
|
61
|
+
manifest: () => {
|
|
62
|
+
const crxPublicKey = import.meta.env.VITE_CRX_PUBLIC_KEY || '';
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
name: ${nameLiteral},
|
|
67
|
+
description: ${descriptionLiteral},
|
|
68
|
+
version: '0.1.0',
|
|
69
|
+
...(crxPublicKey ? { key: crxPublicKey } : {}),
|
|
70
|
+
minimum_chrome_version: '114',
|
|
71
|
+
permissions: ${"['activeTab', 'scripting', 'sidePanel']"},
|
|
72
|
+
host_permissions: [],
|
|
73
|
+
action: {
|
|
74
|
+
default_title: ${actionTitleLiteral},
|
|
75
|
+
},
|
|
76
|
+
side_panel: {
|
|
77
|
+
default_path: 'sidepanel.html',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
modules: ['@wxt-dev/module-react'],
|
|
82
|
+
vite: () => ({
|
|
83
|
+
build: {
|
|
84
|
+
chunkSizeWarningLimit: 3000,
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
function extensionEnvContents() {
|
|
91
|
+
const sections = [];
|
|
92
|
+
sections.push(`# Chrome Extension CRX public key (optional, for stable extension ID)
|
|
93
|
+
VITE_CRX_PUBLIC_KEY=`);
|
|
94
|
+
return `${sections.join('\n\n')}\n`;
|
|
95
|
+
}
|
|
96
|
+
function writeEnvExample(dir) {
|
|
97
|
+
writeFile(path.join(dir, '.env.example'), extensionEnvContents());
|
|
98
|
+
}
|
|
99
|
+
function writePostcssConfig(dir) {
|
|
100
|
+
writeFile(path.join(dir, 'postcss.config.cjs'), `module.exports = {
|
|
101
|
+
plugins: {
|
|
102
|
+
tailwindcss: {},
|
|
103
|
+
autoprefixer: {},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
`);
|
|
107
|
+
}
|
|
108
|
+
function writeShadcnConfig(dir) {
|
|
109
|
+
writeFile(path.join(dir, 'components.json'), JSON.stringify({
|
|
110
|
+
$schema: 'https://ui.shadcn.com/schema.json',
|
|
111
|
+
style: 'new-york',
|
|
112
|
+
rsc: false,
|
|
113
|
+
tsx: true,
|
|
114
|
+
tailwind: {
|
|
115
|
+
config: '',
|
|
116
|
+
css: 'src/styles/main.css',
|
|
117
|
+
baseColor: 'neutral',
|
|
118
|
+
cssVariables: true,
|
|
119
|
+
prefix: '',
|
|
120
|
+
},
|
|
121
|
+
aliases: {
|
|
122
|
+
components: '@/components',
|
|
123
|
+
utils: '@/lib/utils',
|
|
124
|
+
ui: '@/components/ui',
|
|
125
|
+
lib: '@/lib',
|
|
126
|
+
hooks: '@/hooks',
|
|
127
|
+
},
|
|
128
|
+
iconLibrary: 'lucide',
|
|
129
|
+
}, null, 2) + '\n');
|
|
130
|
+
}
|
|
131
|
+
function writeCnUtil(dir) {
|
|
132
|
+
writeFile(path.join(dir, 'src/lib/utils.ts'), `import { clsx, type ClassValue } from 'clsx';
|
|
133
|
+
import { twMerge } from 'tailwind-merge';
|
|
134
|
+
|
|
135
|
+
export function cn(...inputs: ClassValue[]) {
|
|
136
|
+
return twMerge(clsx(inputs));
|
|
137
|
+
}
|
|
138
|
+
`);
|
|
139
|
+
}
|
|
140
|
+
function writeButtonComponent(dir) {
|
|
141
|
+
writeFile(path.join(dir, 'src/components/ui/button.tsx'), `import * as React from 'react';
|
|
142
|
+
import { cn } from '@/lib/utils';
|
|
143
|
+
|
|
144
|
+
type ButtonVariant = 'default' | 'outline';
|
|
145
|
+
|
|
146
|
+
export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
147
|
+
variant?: ButtonVariant;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const variantClasses: Record<ButtonVariant, string> = {
|
|
151
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
152
|
+
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
156
|
+
({ className, variant = 'default', type = 'button', ...props }, ref) => {
|
|
157
|
+
return (
|
|
158
|
+
<button
|
|
159
|
+
ref={ref}
|
|
160
|
+
type={type}
|
|
161
|
+
className={cn(
|
|
162
|
+
'inline-flex h-9 items-center justify-center whitespace-nowrap rounded-md px-4 py-2 text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50',
|
|
163
|
+
variantClasses[variant],
|
|
164
|
+
className,
|
|
165
|
+
)}
|
|
166
|
+
{...props}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
Button.displayName = 'Button';
|
|
173
|
+
`);
|
|
174
|
+
}
|
|
175
|
+
function writeTailwindConfig(dir) {
|
|
176
|
+
writeFile(path.join(dir, 'tailwind.config.cjs'), `/** @type {import('tailwindcss').Config} */
|
|
177
|
+
module.exports = {
|
|
178
|
+
content: ['./src/**/*.{ts,tsx,html}'],
|
|
179
|
+
theme: {
|
|
180
|
+
extend: {
|
|
181
|
+
colors: {
|
|
182
|
+
border: 'hsl(var(--border))',
|
|
183
|
+
input: 'hsl(var(--input))',
|
|
184
|
+
ring: 'hsl(var(--ring))',
|
|
185
|
+
background: 'hsl(var(--background))',
|
|
186
|
+
foreground: 'hsl(var(--foreground))',
|
|
187
|
+
primary: {
|
|
188
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
189
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
190
|
+
},
|
|
191
|
+
secondary: {
|
|
192
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
193
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
194
|
+
},
|
|
195
|
+
destructive: {
|
|
196
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
197
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
198
|
+
},
|
|
199
|
+
muted: {
|
|
200
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
201
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
202
|
+
},
|
|
203
|
+
accent: {
|
|
204
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
205
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
206
|
+
},
|
|
207
|
+
popover: {
|
|
208
|
+
DEFAULT: 'hsl(var(--popover))',
|
|
209
|
+
foreground: 'hsl(var(--popover-foreground))',
|
|
210
|
+
},
|
|
211
|
+
card: {
|
|
212
|
+
DEFAULT: 'hsl(var(--card))',
|
|
213
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
borderRadius: {
|
|
217
|
+
lg: 'var(--radius)',
|
|
218
|
+
md: 'calc(var(--radius) - 2px)',
|
|
219
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
plugins: [],
|
|
224
|
+
};
|
|
225
|
+
`);
|
|
226
|
+
}
|
|
227
|
+
function writeReadme(dir, displayName) {
|
|
228
|
+
const configuration = [
|
|
229
|
+
null,
|
|
230
|
+
null,
|
|
231
|
+
'Set `VITE_CRX_PUBLIC_KEY` only when you need a stable Chrome extension ID.',
|
|
232
|
+
].filter((value) => value !== null).join(' ');
|
|
233
|
+
const networkIntegrations = [].join(' and ');
|
|
234
|
+
const hostPermissionsGuidance = networkIntegrations
|
|
235
|
+
? `\n\nThe manifest derives deduplicated Chrome host permissions from ${networkIntegrations}. Only absolute HTTP(S) URLs are accepted; malformed configured URLs fail the build. Set production URLs in the extension environment before publishing—no source edit is required.`
|
|
236
|
+
: '';
|
|
237
|
+
writeFile(path.join(dir, 'README.md'), `# ${markdownHeading(displayName)} Chrome Extension
|
|
238
|
+
|
|
239
|
+
WXT side-panel extension generated by anhedral.
|
|
240
|
+
|
|
241
|
+
## Development
|
|
242
|
+
|
|
243
|
+
\`\`\`bash
|
|
244
|
+
cp .env.example .env
|
|
245
|
+
pnpm dev
|
|
246
|
+
pnpm build
|
|
247
|
+
pnpm zip
|
|
248
|
+
\`\`\`
|
|
249
|
+
|
|
250
|
+
Run these commands from \`apps/extension\`. Anhedral generates only \`.env.example\`; keep the local \`.env\` file uncommitted. ${configuration}
|
|
251
|
+
|
|
252
|
+
## Chrome
|
|
253
|
+
|
|
254
|
+
Run \`pnpm build\`, then load \`.output/chrome-mv3\` as an unpacked extension from \`chrome://extensions\`.
|
|
255
|
+
|
|
256
|
+
The extension uses Chrome's Side Panel API. The browser action opens \`sidepanel.html\`, \`wxt.config.ts\` declares the \`sidePanel\` permission and Chrome 114+ minimum version, and active-page reads use a user-triggered \`activeTab\` + \`scripting\` grant instead of persistent site access.${hostPermissionsGuidance}
|
|
257
|
+
`);
|
|
258
|
+
}
|
|
259
|
+
function writeBackground(dir) {
|
|
260
|
+
writeFile(path.join(dir, 'src/entrypoints/background.ts'), `type ChromeWithSidePanel = typeof chrome & {
|
|
261
|
+
sidePanel: {
|
|
262
|
+
setPanelBehavior: (behavior: { openPanelOnActionClick: boolean }) => Promise<void>;
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
export default defineBackground(() => {
|
|
267
|
+
|
|
268
|
+
// Open the side panel when the extension icon is clicked.
|
|
269
|
+
(chrome as ChromeWithSidePanel).sidePanel
|
|
270
|
+
.setPanelBehavior({ openPanelOnActionClick: true })
|
|
271
|
+
.catch(() => {});
|
|
272
|
+
});
|
|
273
|
+
`);
|
|
274
|
+
}
|
|
275
|
+
function writeSidepanelEntry(dir) {
|
|
276
|
+
writeFile(path.join(dir, 'src/entrypoints/sidepanel/main.tsx'), `import * as React from 'react';
|
|
277
|
+
import { createRoot } from 'react-dom/client';
|
|
278
|
+
import { SidePanelApp } from './app';
|
|
279
|
+
import '../../styles/main.css';
|
|
280
|
+
|
|
281
|
+
const container = document.getElementById('root');
|
|
282
|
+
if (container) {
|
|
283
|
+
const root = createRoot(container);
|
|
284
|
+
root.render(
|
|
285
|
+
<React.StrictMode>
|
|
286
|
+
${" <SidePanelApp />"}
|
|
287
|
+
</React.StrictMode>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
`);
|
|
291
|
+
}
|
|
292
|
+
function writeSidepanelHtml(dir, displayName) {
|
|
293
|
+
writeFile(path.join(dir, 'src/entrypoints/sidepanel/index.html'), `<!DOCTYPE html>
|
|
294
|
+
<html lang="en">
|
|
295
|
+
<head>
|
|
296
|
+
<meta charset="UTF-8" />
|
|
297
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
298
|
+
<title>${htmlText(displayName)}</title>
|
|
299
|
+
</head>
|
|
300
|
+
<body>
|
|
301
|
+
<div id="root"></div>
|
|
302
|
+
<script type="module" src="./main.tsx"></script>
|
|
303
|
+
</body>
|
|
304
|
+
</html>
|
|
305
|
+
`);
|
|
306
|
+
}
|
|
307
|
+
function writeSidepanelApp(dir) {
|
|
308
|
+
writeFile(path.join(dir, 'src/entrypoints/sidepanel/app.tsx'), `import * as React from 'react';
|
|
309
|
+
import { Button } from '../../components/ui/button';
|
|
310
|
+
|
|
311
|
+
type PageSnapshot = {
|
|
312
|
+
title: string;
|
|
313
|
+
location: string;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export function SidePanelApp() {
|
|
317
|
+
const [page, setPage] = React.useState<PageSnapshot | null>(null);
|
|
318
|
+
const [pageError, setPageError] = React.useState<string | null>(null);
|
|
319
|
+
|
|
320
|
+
const readActivePage = React.useCallback(async () => {
|
|
321
|
+
setPageError(null);
|
|
322
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
323
|
+
if (!tab.id) {
|
|
324
|
+
setPageError('No active tab is available.');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const [injection] = await chrome.scripting.executeScript({
|
|
330
|
+
target: { tabId: tab.id },
|
|
331
|
+
func: () => ({ title: document.title, location: window.location.href }),
|
|
332
|
+
});
|
|
333
|
+
if (!injection?.result) throw new Error('The active page did not return a snapshot.');
|
|
334
|
+
setPage(injection.result as PageSnapshot);
|
|
335
|
+
} catch {
|
|
336
|
+
setPageError('Chrome does not allow this page to be inspected. Open a normal website and try again.');
|
|
337
|
+
}
|
|
338
|
+
}, []);
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
return (
|
|
342
|
+
<div style={{ padding: 24 }}>
|
|
343
|
+
<h2>Welcome!</h2>
|
|
344
|
+
<Button type="button" onClick={() => void readActivePage()}>Read active page</Button>
|
|
345
|
+
{page ? (
|
|
346
|
+
<div role="status" aria-live="polite" style={{ marginTop: 16 }}>
|
|
347
|
+
<strong>{page.title || 'Untitled page'}</strong>
|
|
348
|
+
<p style={{ overflowWrap: 'anywhere' }}>{page.location}</p>
|
|
349
|
+
</div>
|
|
350
|
+
) : null}
|
|
351
|
+
{pageError ? <p role="alert" aria-live="assertive" style={{ color: 'hsl(var(--destructive))' }}>{pageError}</p> : null}
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
`);
|
|
356
|
+
}
|
|
357
|
+
function writeStyles(dir) {
|
|
358
|
+
writeFile(path.join(dir, 'src/styles/main.css'), `@tailwind base;
|
|
359
|
+
@tailwind components;
|
|
360
|
+
@tailwind utilities;
|
|
361
|
+
|
|
362
|
+
@layer base {
|
|
363
|
+
:root {
|
|
364
|
+
--background: 0 0% 100%;
|
|
365
|
+
--foreground: 0 0% 9%;
|
|
366
|
+
--card: 0 0% 100%;
|
|
367
|
+
--card-foreground: 0 0% 9%;
|
|
368
|
+
--popover: 0 0% 100%;
|
|
369
|
+
--popover-foreground: 0 0% 9%;
|
|
370
|
+
--primary: 0 0% 9%;
|
|
371
|
+
--primary-foreground: 0 0% 98%;
|
|
372
|
+
--secondary: 0 0% 96.1%;
|
|
373
|
+
--secondary-foreground: 0 0% 9%;
|
|
374
|
+
--muted: 0 0% 96.1%;
|
|
375
|
+
--muted-foreground: 0 0% 45.1%;
|
|
376
|
+
--accent: 0 0% 96.1%;
|
|
377
|
+
--accent-foreground: 0 0% 9%;
|
|
378
|
+
--destructive: 0 84.2% 60.2%;
|
|
379
|
+
--destructive-foreground: 0 0% 98%;
|
|
380
|
+
--border: 0 0% 89.8%;
|
|
381
|
+
--input: 0 0% 89.8%;
|
|
382
|
+
--ring: 0 0% 63.9%;
|
|
383
|
+
--radius: 0.5rem;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
* {
|
|
387
|
+
border-color: hsl(var(--border));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
body {
|
|
391
|
+
background: hsl(var(--background));
|
|
392
|
+
color: hsl(var(--foreground));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
body {
|
|
397
|
+
margin: 0;
|
|
398
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
399
|
+
}
|
|
400
|
+
`);
|
|
401
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { appendGitignore, writeFile } from '../util.js';
|
|
3
|
+
import { childPackageName, jsString } from '../render.js';
|
|
4
|
+
import { MOBILE_APP_DEPENDENCIES, MOBILE_NATIVEWIND_DEPENDENCIES, EAS_CLI_VERSION, } from '../dependencies.js';
|
|
5
|
+
function writeReactNativeReusablesConfig(dir) {
|
|
6
|
+
writeFile(path.join(dir, 'components.json'), JSON.stringify({
|
|
7
|
+
$schema: 'https://ui.shadcn.com/schema.json',
|
|
8
|
+
style: 'new-york',
|
|
9
|
+
rsc: false,
|
|
10
|
+
tsx: true,
|
|
11
|
+
tailwind: {
|
|
12
|
+
config: 'tailwind.config.js',
|
|
13
|
+
css: 'global.css',
|
|
14
|
+
baseColor: 'neutral',
|
|
15
|
+
cssVariables: true,
|
|
16
|
+
},
|
|
17
|
+
aliases: {
|
|
18
|
+
components: '@/components',
|
|
19
|
+
utils: '@/lib/utils',
|
|
20
|
+
ui: '@/components/ui',
|
|
21
|
+
lib: '@/lib',
|
|
22
|
+
hooks: '@/hooks',
|
|
23
|
+
},
|
|
24
|
+
iconLibrary: 'lucide',
|
|
25
|
+
}, null, 2) + '\n');
|
|
26
|
+
writeFile(path.join(dir, 'lib/utils.ts'), `import { clsx, type ClassValue } from 'clsx';
|
|
27
|
+
import { twMerge } from 'tailwind-merge';
|
|
28
|
+
|
|
29
|
+
export function cn(...inputs: ClassValue[]) {
|
|
30
|
+
return twMerge(clsx(inputs));
|
|
31
|
+
}
|
|
32
|
+
`);
|
|
33
|
+
writeFile(path.join(dir, 'babel.config.js'), `module.exports = function (api) {
|
|
34
|
+
api.cache(true);
|
|
35
|
+
return {
|
|
36
|
+
presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
`);
|
|
40
|
+
writeFile(path.join(dir, 'metro.config.js'), `const { getDefaultConfig } = require('expo/metro-config');
|
|
41
|
+
const { withNativeWind } = require('nativewind/metro');
|
|
42
|
+
|
|
43
|
+
const config = getDefaultConfig(__dirname);
|
|
44
|
+
|
|
45
|
+
module.exports = withNativeWind(config, { input: './global.css', inlineRem: 16 });
|
|
46
|
+
`);
|
|
47
|
+
writeFile(path.join(dir, 'nativewind-env.d.ts'), '/// <reference types="nativewind/types" />\n');
|
|
48
|
+
writeFile(path.join(dir, 'tailwind.config.js'), `const { hairlineWidth } = require('nativewind/theme');
|
|
49
|
+
|
|
50
|
+
/** @type {import('tailwindcss').Config} */
|
|
51
|
+
module.exports = {
|
|
52
|
+
darkMode: 'class',
|
|
53
|
+
content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
|
|
54
|
+
presets: [require('nativewind/preset')],
|
|
55
|
+
theme: {
|
|
56
|
+
extend: {
|
|
57
|
+
colors: {
|
|
58
|
+
border: 'hsl(var(--border))', input: 'hsl(var(--input))', ring: 'hsl(var(--ring))',
|
|
59
|
+
background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))',
|
|
60
|
+
primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))' },
|
|
61
|
+
secondary: { DEFAULT: 'hsl(var(--secondary))', foreground: 'hsl(var(--secondary-foreground))' },
|
|
62
|
+
destructive: { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))' },
|
|
63
|
+
muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))' },
|
|
64
|
+
accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))' },
|
|
65
|
+
popover: { DEFAULT: 'hsl(var(--popover))', foreground: 'hsl(var(--popover-foreground))' },
|
|
66
|
+
card: { DEFAULT: 'hsl(var(--card))', foreground: 'hsl(var(--card-foreground))' },
|
|
67
|
+
},
|
|
68
|
+
borderRadius: { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)' },
|
|
69
|
+
borderWidth: { hairline: hairlineWidth() },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
future: { hoverOnlyWhenSupported: true },
|
|
73
|
+
plugins: [require('tailwindcss-animate')],
|
|
74
|
+
};
|
|
75
|
+
`);
|
|
76
|
+
writeFile(path.join(dir, 'global.css'), `@tailwind base;
|
|
77
|
+
@tailwind components;
|
|
78
|
+
@tailwind utilities;
|
|
79
|
+
|
|
80
|
+
@layer base {
|
|
81
|
+
:root {
|
|
82
|
+
--background: 0 0% 100%; --foreground: 0 0% 3.9%;
|
|
83
|
+
--card: 0 0% 100%; --card-foreground: 0 0% 3.9%;
|
|
84
|
+
--popover: 0 0% 100%; --popover-foreground: 0 0% 3.9%;
|
|
85
|
+
--primary: 0 0% 9%; --primary-foreground: 0 0% 98%;
|
|
86
|
+
--secondary: 0 0% 96.1%; --secondary-foreground: 0 0% 9%;
|
|
87
|
+
--muted: 0 0% 96.1%; --muted-foreground: 0 0% 45.1%;
|
|
88
|
+
--accent: 0 0% 96.1%; --accent-foreground: 0 0% 9%;
|
|
89
|
+
--destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%;
|
|
90
|
+
--border: 0 0% 89.8%; --input: 0 0% 89.8%; --ring: 0 0% 63%; --radius: 0.625rem;
|
|
91
|
+
}
|
|
92
|
+
.dark:root {
|
|
93
|
+
--background: 0 0% 3.9%; --foreground: 0 0% 98%;
|
|
94
|
+
--card: 0 0% 3.9%; --card-foreground: 0 0% 98%;
|
|
95
|
+
--popover: 0 0% 3.9%; --popover-foreground: 0 0% 98%;
|
|
96
|
+
--primary: 0 0% 98%; --primary-foreground: 0 0% 9%;
|
|
97
|
+
--secondary: 0 0% 14.9%; --secondary-foreground: 0 0% 98%;
|
|
98
|
+
--muted: 0 0% 14.9%; --muted-foreground: 0 0% 63.9%;
|
|
99
|
+
--accent: 0 0% 14.9%; --accent-foreground: 0 0% 98%;
|
|
100
|
+
--destructive: 0 70.9% 59.4%; --destructive-foreground: 0 0% 98%;
|
|
101
|
+
--border: 0 0% 14.9%; --input: 0 0% 14.9%; --ring: 300 0% 45%;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
`);
|
|
105
|
+
}
|
|
106
|
+
function expoScheme(projectName) {
|
|
107
|
+
const unscoped = projectName.replace(/^@[^/]+\//, '');
|
|
108
|
+
const safe = unscoped
|
|
109
|
+
.toLowerCase()
|
|
110
|
+
.replace(/[^a-z0-9+.-]+/g, '-');
|
|
111
|
+
let start = 0;
|
|
112
|
+
let end = safe.length;
|
|
113
|
+
while (start < end && '+.-'.includes(safe[start]))
|
|
114
|
+
start++;
|
|
115
|
+
while (end > start && '+.-'.includes(safe[end - 1]))
|
|
116
|
+
end--;
|
|
117
|
+
const normalized = safe.slice(start, end);
|
|
118
|
+
const candidate = normalized || 'app';
|
|
119
|
+
return /^[a-z]/.test(candidate) ? candidate : `app-${candidate}`;
|
|
120
|
+
}
|
|
121
|
+
export async function scaffoldMobile(root, options) {
|
|
122
|
+
const dir = path.join(root, 'apps/mobile');
|
|
123
|
+
const { projectName, displayName } = options;
|
|
124
|
+
const nameLiteral = jsString(displayName);
|
|
125
|
+
writeFile(path.join(dir, 'package.json'), JSON.stringify({
|
|
126
|
+
name: childPackageName(projectName, 'mobile'),
|
|
127
|
+
version: '0.1.0',
|
|
128
|
+
private: true,
|
|
129
|
+
main: 'expo-router/entry',
|
|
130
|
+
scripts: {
|
|
131
|
+
dev: 'expo start -c',
|
|
132
|
+
android: 'expo start -c --android',
|
|
133
|
+
ios: 'expo start -c --ios',
|
|
134
|
+
web: 'expo start -c --web',
|
|
135
|
+
build: 'pnpm typecheck && pnpm build:web',
|
|
136
|
+
'build:web': 'expo export --platform web',
|
|
137
|
+
'eas:login': `pnpm dlx eas-cli@${EAS_CLI_VERSION} login`,
|
|
138
|
+
'build:internal:ios': `pnpm dlx eas-cli@${EAS_CLI_VERSION} build --platform ios --profile preview`,
|
|
139
|
+
'build:internal:android': `pnpm dlx eas-cli@${EAS_CLI_VERSION} build --platform android --profile preview`,
|
|
140
|
+
'build:production:ios': `pnpm dlx eas-cli@${EAS_CLI_VERSION} build --platform ios --profile production`,
|
|
141
|
+
'build:production:android': `pnpm dlx eas-cli@${EAS_CLI_VERSION} build --platform android --profile production`,
|
|
142
|
+
'submit:ios': `pnpm dlx eas-cli@${EAS_CLI_VERSION} submit --platform ios --profile production --latest`,
|
|
143
|
+
'submit:android': `pnpm dlx eas-cli@${EAS_CLI_VERSION} submit --platform android --profile production --latest`,
|
|
144
|
+
typecheck: 'tsc --noEmit',
|
|
145
|
+
},
|
|
146
|
+
dependencies: { ...MOBILE_APP_DEPENDENCIES.dependencies, ...MOBILE_NATIVEWIND_DEPENDENCIES.dependencies },
|
|
147
|
+
devDependencies: MOBILE_APP_DEPENDENCIES.devDependencies,
|
|
148
|
+
}, null, 2) + '\n');
|
|
149
|
+
writeFile(path.join(dir, 'app.json'), JSON.stringify({
|
|
150
|
+
expo: {
|
|
151
|
+
name: displayName,
|
|
152
|
+
slug: projectName.replace(/^@[^/]+\//, ''),
|
|
153
|
+
version: '1.0.0',
|
|
154
|
+
orientation: 'portrait',
|
|
155
|
+
scheme: expoScheme(projectName),
|
|
156
|
+
userInterfaceStyle: 'automatic',
|
|
157
|
+
web: { bundler: 'metro', output: 'static' },
|
|
158
|
+
plugins: ['expo-router', ...([]), 'expo-status-bar'],
|
|
159
|
+
experiments: { reactCompiler: true, typedRoutes: true },
|
|
160
|
+
},
|
|
161
|
+
}, null, 2) + '\n');
|
|
162
|
+
writeFile(path.join(dir, 'tsconfig.json'), JSON.stringify({
|
|
163
|
+
extends: 'expo/tsconfig.base',
|
|
164
|
+
compilerOptions: {
|
|
165
|
+
strict: true,
|
|
166
|
+
noUncheckedIndexedAccess: true,
|
|
167
|
+
paths: { '@/*': ['./*'] },
|
|
168
|
+
},
|
|
169
|
+
include: [
|
|
170
|
+
'**/*.ts',
|
|
171
|
+
'**/*.tsx',
|
|
172
|
+
'.expo/types/**/*.ts',
|
|
173
|
+
'expo-env.d.ts',
|
|
174
|
+
'nativewind-env.d.ts',
|
|
175
|
+
],
|
|
176
|
+
}, null, 2) + '\n');
|
|
177
|
+
writeReactNativeReusablesConfig(dir);
|
|
178
|
+
const layoutBody = ` return <>${"<Stack />"}<PortalHost /></>;`;
|
|
179
|
+
writeFile(path.join(dir, 'app/_layout.tsx'), `import '@/global.css';
|
|
180
|
+
|
|
181
|
+
import { PortalHost } from '@rn-primitives/portal';
|
|
182
|
+
import { Stack } from 'expo-router';
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
export default function RootLayout() {
|
|
186
|
+
${layoutBody}
|
|
187
|
+
}
|
|
188
|
+
`);
|
|
189
|
+
const homeImports = [].join('\n');
|
|
190
|
+
writeFile(path.join(dir, 'app/index.tsx'), `import { StatusBar } from 'expo-status-bar';
|
|
191
|
+
import { ${"ScrollView, StyleSheet, Text, View"} } from 'react-native';
|
|
192
|
+
${homeImports}
|
|
193
|
+
|
|
194
|
+
const enabledModules = [];
|
|
195
|
+
|
|
196
|
+
export default function HomeScreen() {
|
|
197
|
+
return (
|
|
198
|
+
<ScrollView style={styles.screen} contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.container}>
|
|
199
|
+
<StatusBar style="auto" />
|
|
200
|
+
<View style={styles.content}>
|
|
201
|
+
<Text accessibilityRole="header" style={styles.title}>{${nameLiteral}}</Text>
|
|
202
|
+
<Text selectable style={styles.subtitle}>Deterministic Expo application generated by Anhedral.</Text>
|
|
203
|
+
{enabledModules.map((moduleName) => <Text selectable key={moduleName} style={styles.module}>• {moduleName}</Text>)}
|
|
204
|
+
</View>
|
|
205
|
+
</ScrollView>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const styles = StyleSheet.create({
|
|
210
|
+
screen: { flex: 1, backgroundColor: '#f8fafc' },
|
|
211
|
+
container: { flexGrow: 1, justifyContent: 'center', padding: 32 },
|
|
212
|
+
content: { gap: 12 },
|
|
213
|
+
actions: { gap: 8, paddingTop: 8 },
|
|
214
|
+
title: { color: '#0f172a', fontSize: 34, fontWeight: '700' },
|
|
215
|
+
subtitle: { color: '#475569', fontSize: 17, paddingBottom: 12 },
|
|
216
|
+
module: { color: '#1e293b', fontSize: 15 },
|
|
217
|
+
status: { color: '#475569', fontSize: 14 },
|
|
218
|
+
error: { color: '#b91c1c', fontSize: 14 },
|
|
219
|
+
});
|
|
220
|
+
`);
|
|
221
|
+
writeFile(path.join(dir, '.env.example'), '');
|
|
222
|
+
writeFile(path.join(dir, 'eas.json'), JSON.stringify({
|
|
223
|
+
cli: { version: '>= 16.0.0' },
|
|
224
|
+
build: {
|
|
225
|
+
development: { developmentClient: true, distribution: 'internal' },
|
|
226
|
+
preview: { distribution: 'internal' },
|
|
227
|
+
production: { autoIncrement: true },
|
|
228
|
+
},
|
|
229
|
+
submit: { production: {} },
|
|
230
|
+
}, null, 2) + '\n');
|
|
231
|
+
appendGitignore(dir, ['.env', '.env.*', '!.env.example', '.expo', 'dist', 'node_modules']);
|
|
232
|
+
}
|
package/dist/project.d.ts
CHANGED
|
@@ -1,30 +1,4 @@
|
|
|
1
|
-
import type { NativeStylingLibrary } from './ui.js';
|
|
2
|
-
export type AppSelections = {
|
|
3
|
-
web: boolean;
|
|
4
|
-
mobile: boolean;
|
|
5
|
-
api: boolean;
|
|
6
|
-
desktop: boolean;
|
|
7
|
-
extension: boolean;
|
|
8
|
-
};
|
|
9
|
-
export type FeatureSelections = {
|
|
10
|
-
database: boolean;
|
|
11
|
-
auth: boolean;
|
|
12
|
-
billing: boolean;
|
|
13
|
-
storage: boolean;
|
|
14
|
-
nativeSubscriptions: boolean;
|
|
15
|
-
electronUpdater: boolean;
|
|
16
|
-
};
|
|
17
|
-
/**
|
|
18
|
-
* The project shape consumed by every template.
|
|
19
|
-
*
|
|
20
|
-
* Keep this independent from the scaffold orchestrator so templates depend on
|
|
21
|
-
* the project model, not on the command that happens to create it.
|
|
22
|
-
*/
|
|
23
1
|
export interface ProjectOptions {
|
|
24
2
|
projectName: string;
|
|
25
3
|
displayName: string;
|
|
26
|
-
apps: AppSelections;
|
|
27
|
-
features: FeatureSelections;
|
|
28
|
-
skipInstall?: boolean;
|
|
29
|
-
nativeStyling?: NativeStylingLibrary;
|
|
30
4
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const STANDARD_PRODUCTS: readonly ["next", "expo", "electron", "wxt", "hono", "neon", "d1", "local-data", "clerk", "better-auth", "r2", "kv", "realtime", "queues", "cron", "workflows", "openai", "ai-sdk", "workers-ai", "resend", "stripe", "revenuecat", "sentry", "basin", "posthog", "styling"];
|
|
2
|
+
export type StandardProduct = typeof STANDARD_PRODUCTS[number];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const STANDARD_PRODUCTS = [
|
|
2
|
+
'next', 'expo', 'electron', 'wxt', 'hono', 'neon', 'd1', 'local-data',
|
|
3
|
+
'clerk', 'better-auth', 'r2', 'kv', 'realtime', 'queues', 'cron', 'workflows',
|
|
4
|
+
'openai', 'ai-sdk', 'workers-ai', 'resend', 'stripe', 'revenuecat',
|
|
5
|
+
'sentry', 'basin', 'posthog', 'styling',
|
|
6
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type StandardProduct } from './standard-products.js';
|
|
2
|
+
export { STANDARD_PRODUCTS } from './standard-products.js';
|
|
3
|
+
export type { StandardProduct } from './standard-products.js';
|
|
4
|
+
export interface StandardOptions {
|
|
5
|
+
root: string;
|
|
6
|
+
name: string;
|
|
7
|
+
products: StandardProduct[];
|
|
8
|
+
hosting: 'cloudflare' | 'vercel';
|
|
9
|
+
skipInstall: boolean;
|
|
10
|
+
dryRun: boolean;
|
|
11
|
+
json: boolean;
|
|
12
|
+
initializeGit: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare const STANDARD_USAGE: string;
|
|
15
|
+
export declare function parseStandardOptions(command: 'new' | 'init', args: readonly string[]): StandardOptions;
|
|
16
|
+
export declare function applyStandardOverlays(root: string, options: StandardOptions): Promise<void>;
|
|
17
|
+
export type StandardRunner = (command: string, args: string[], cwd: string) => void;
|
|
18
|
+
export declare function scaffoldStandardProject(options: StandardOptions, run?: StandardRunner): Promise<void>;
|