anhedral 0.8.6 → 0.9.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 +141 -38
- package/assets/anhedral-cli-init-technical.svg +15 -15
- package/assets/anhedral-cli-init.svg +16 -16
- package/dist/access.d.ts +10 -0
- package/dist/access.js +93 -0
- package/dist/auth-schema-snapshot.d.ts +346 -0
- package/dist/auth-schema-snapshot.js +347 -0
- package/dist/auth-schema.d.ts +1 -0
- package/dist/auth-schema.js +57 -0
- package/dist/bin.js +345 -39
- package/dist/bounded-file.d.ts +2 -0
- package/dist/bounded-file.js +30 -0
- package/dist/capabilities.d.ts +24 -42
- package/dist/capabilities.js +73 -75
- package/dist/cloudflare.d.ts +14 -0
- package/dist/cloudflare.js +66 -0
- package/dist/command.js +12 -3
- package/dist/delivery.d.ts +118 -0
- package/dist/delivery.js +611 -0
- package/dist/dependencies.d.ts +24 -3
- package/dist/dependencies.js +112 -74
- package/dist/dependency-order.d.ts +2 -0
- package/dist/dependency-order.js +24 -0
- package/dist/index.d.ts +18 -3
- package/dist/index.js +12 -2
- package/dist/managed-hosting.d.ts +49 -0
- package/dist/managed-hosting.js +373 -0
- package/dist/management.d.ts +3 -0
- package/dist/management.js +47 -0
- package/dist/platforms/desktop.d.ts +2 -2
- package/dist/platforms/desktop.js +25 -15
- package/dist/platforms/extension.d.ts +2 -2
- package/dist/platforms/extension.js +104 -66
- package/dist/platforms/mobile.d.ts +2 -2
- package/dist/platforms/mobile.js +13 -7
- package/dist/pnpm-policy.d.ts +0 -2
- package/dist/pnpm-policy.js +18 -13
- package/dist/project-state.d.ts +28 -0
- package/dist/project-state.js +226 -0
- package/dist/provider-readiness.d.ts +30 -0
- package/dist/provider-readiness.js +87 -0
- package/dist/provision.d.ts +46 -0
- package/dist/provision.js +197 -0
- package/dist/readiness.d.ts +4 -2
- package/dist/readiness.js +81 -14
- package/dist/registry/add.d.ts +22 -0
- package/dist/registry/add.js +198 -0
- package/dist/registry/bootstrap.d.ts +2 -0
- package/dist/registry/bootstrap.js +14 -0
- package/dist/registry/commands.d.ts +2 -0
- package/dist/registry/commands.js +99 -0
- package/dist/registry/context.d.ts +3 -0
- package/dist/registry/context.js +21 -0
- package/dist/registry/options.d.ts +2 -0
- package/dist/registry/options.js +65 -0
- package/dist/registry/receipt.d.ts +3 -0
- package/dist/registry/receipt.js +73 -0
- package/dist/registry/remote.d.ts +25 -0
- package/dist/registry/remote.js +168 -0
- package/dist/registry/resolve.d.ts +10 -0
- package/dist/registry/resolve.js +98 -0
- package/dist/registry/types.d.ts +64 -0
- package/dist/registry/usage.d.ts +1 -0
- package/dist/registry/usage.js +38 -0
- package/dist/render.d.ts +0 -4
- package/dist/render.js +3 -16
- package/dist/repository.d.ts +24 -0
- package/dist/repository.js +96 -0
- package/dist/scaffold/api.d.ts +2 -0
- package/dist/scaffold/api.js +13 -0
- package/dist/scaffold/auth.d.ts +3 -0
- package/dist/scaffold/auth.js +95 -0
- package/dist/scaffold/bootstrap.d.ts +2 -0
- package/dist/scaffold/bootstrap.js +34 -0
- package/dist/scaffold/compose.d.ts +4 -0
- package/dist/scaffold/compose.js +34 -0
- package/dist/scaffold/database.d.ts +2 -0
- package/dist/scaffold/database.js +43 -0
- package/dist/scaffold/documentation.d.ts +3 -0
- package/dist/scaffold/documentation.js +91 -0
- package/dist/scaffold/environment.d.ts +2 -0
- package/dist/scaffold/environment.js +5 -0
- package/dist/scaffold/files.d.ts +25 -0
- package/dist/scaffold/files.js +108 -0
- package/dist/scaffold/integrations.d.ts +3 -0
- package/dist/scaffold/integrations.js +7 -0
- package/dist/scaffold/services.d.ts +5 -0
- package/dist/scaffold/services.js +84 -0
- package/dist/scaffold/web.d.ts +2 -0
- package/dist/scaffold/web.js +102 -0
- package/dist/scaffold/workspace.d.ts +3 -0
- package/dist/scaffold/workspace.js +150 -0
- package/dist/security-backports.js +7 -2
- package/dist/stack-registry.d.ts +80 -0
- package/dist/stack-registry.js +424 -0
- package/dist/standard.d.ts +6 -19
- package/dist/standard.js +46 -653
- package/dist/transaction.js +47 -56
- package/dist/util.js +12 -2
- package/dist/vercel.d.ts +98 -0
- package/dist/vercel.js +796 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/vinext.d.ts +1 -0
- package/dist/vinext.js +83 -0
- package/dist/workspace-graph.d.ts +7 -0
- package/dist/workspace-graph.js +73 -0
- package/docs/application-stack-standard.md +76 -91
- package/docs/delivery-workflow.md +83 -0
- package/docs/managed-hosting-architecture.md +87 -0
- package/docs/sponsorships.md +25 -0
- package/package.json +23 -29
- package/dist/evidence-validation.d.ts +0 -2
- package/dist/evidence-validation.js +0 -18
- package/dist/foundation.d.ts +0 -6
- package/dist/foundation.js +0 -49
- package/dist/progress-view.d.ts +0 -57
- package/dist/progress-view.js +0 -69
- package/dist/progress.d.ts +0 -95
- package/dist/progress.js +0 -327
- package/dist/project.d.ts +0 -5
- package/dist/standard-products.d.ts +0 -2
- package/dist/standard-products.js +0 -6
- /package/assets/{images/svg/logo-white-subtract.svg → anhedral-mark.svg} +0 -0
- /package/dist/{project.js → registry/types.js} +0 -0
|
@@ -3,24 +3,28 @@ import { writeReactLintConfig } from './lint.js';
|
|
|
3
3
|
import { writeFile } from '../util.js';
|
|
4
4
|
import { EXTENSION_DEPENDENCIES } from '../dependencies.js';
|
|
5
5
|
import { childPackageName, htmlText, jsString, markdownHeading } from '../render.js';
|
|
6
|
-
export async function
|
|
7
|
-
const
|
|
6
|
+
export async function scaffoldWxt({ root, options }) {
|
|
7
|
+
const projectName = options.name;
|
|
8
|
+
const displayName = options.name;
|
|
8
9
|
const dir = path.join(root, 'apps/extension');
|
|
9
10
|
writePackageJson(dir, projectName);
|
|
10
|
-
writeFile(path.join(dir, 'tsconfig.json'), JSON.stringify({
|
|
11
|
+
writeFile(path.join(dir, 'tsconfig.json'), JSON.stringify({
|
|
12
|
+
extends: './.wxt/tsconfig.json',
|
|
13
|
+
compilerOptions: { strict: true, jsx: 'react-jsx', paths: { '@/*': ['./src/*'] } },
|
|
14
|
+
}, null, 2) + '\n');
|
|
11
15
|
writeReactLintConfig(dir);
|
|
12
|
-
writeWxtConfig(dir, displayName
|
|
16
|
+
writeWxtConfig(dir, displayName);
|
|
13
17
|
writeEnvExample(dir);
|
|
14
18
|
writePostcssConfig(dir);
|
|
19
|
+
writeTailwindConfig(dir);
|
|
15
20
|
writeShadcnConfig(dir);
|
|
16
|
-
writeReadme(dir, displayName
|
|
21
|
+
writeReadme(dir, displayName);
|
|
17
22
|
writeCnUtil(dir);
|
|
18
23
|
writeButtonComponent(dir);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
writeSurfaceApp(dir, extensionSurface);
|
|
24
|
+
writeBackground(dir);
|
|
25
|
+
writeSidepanelEntry(dir);
|
|
26
|
+
writeSidepanelHtml(dir, displayName);
|
|
27
|
+
writeSidepanelApp(dir);
|
|
24
28
|
writeStyles(dir);
|
|
25
29
|
}
|
|
26
30
|
function writePackageJson(dir, projectName) {
|
|
@@ -38,10 +42,14 @@ function writePackageJson(dir, projectName) {
|
|
|
38
42
|
lint: 'eslint src --max-warnings 0',
|
|
39
43
|
},
|
|
40
44
|
dependencies: EXTENSION_DEPENDENCIES.dependencies,
|
|
41
|
-
devDependencies: {
|
|
45
|
+
devDependencies: {
|
|
46
|
+
...EXTENSION_DEPENDENCIES.devDependencies,
|
|
47
|
+
'@workspace/eslint-config': 'workspace:*',
|
|
48
|
+
eslint: '10.0.3',
|
|
49
|
+
},
|
|
42
50
|
}, null, 2) + '\n');
|
|
43
51
|
}
|
|
44
|
-
function writeWxtConfig(dir, displayName
|
|
52
|
+
function writeWxtConfig(dir, displayName) {
|
|
45
53
|
const nameLiteral = jsString(displayName);
|
|
46
54
|
const descriptionLiteral = jsString(`${displayName} Chrome Extension`);
|
|
47
55
|
const actionTitleLiteral = jsString(`Open ${displayName}`);
|
|
@@ -59,14 +67,15 @@ export default defineConfig({
|
|
|
59
67
|
description: ${descriptionLiteral},
|
|
60
68
|
version: '0.1.0',
|
|
61
69
|
...(crxPublicKey ? { key: crxPublicKey } : {}),
|
|
62
|
-
|
|
63
|
-
permissions:
|
|
70
|
+
minimum_chrome_version: '114',
|
|
71
|
+
permissions: ['activeTab', 'scripting', 'sidePanel'],
|
|
64
72
|
host_permissions: [],
|
|
65
73
|
action: {
|
|
66
74
|
default_title: ${actionTitleLiteral},
|
|
67
|
-
${surface === 'popup' ? "default_popup: 'popup.html'," : ''}
|
|
68
75
|
},
|
|
69
|
-
|
|
76
|
+
side_panel: {
|
|
77
|
+
default_path: 'sidepanel.html',
|
|
78
|
+
},
|
|
70
79
|
};
|
|
71
80
|
},
|
|
72
81
|
modules: ['@wxt-dev/module-react'],
|
|
@@ -90,7 +99,8 @@ function writeEnvExample(dir) {
|
|
|
90
99
|
function writePostcssConfig(dir) {
|
|
91
100
|
writeFile(path.join(dir, 'postcss.config.cjs'), `module.exports = {
|
|
92
101
|
plugins: {
|
|
93
|
-
|
|
102
|
+
tailwindcss: {},
|
|
103
|
+
autoprefixer: {},
|
|
94
104
|
},
|
|
95
105
|
};
|
|
96
106
|
`);
|
|
@@ -162,10 +172,62 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
162
172
|
Button.displayName = 'Button';
|
|
163
173
|
`);
|
|
164
174
|
}
|
|
165
|
-
function
|
|
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) {
|
|
166
228
|
writeFile(path.join(dir, 'README.md'), `# ${markdownHeading(displayName)} Chrome Extension
|
|
167
229
|
|
|
168
|
-
WXT
|
|
230
|
+
WXT side-panel extension generated by anhedral.
|
|
169
231
|
|
|
170
232
|
## Development
|
|
171
233
|
|
|
@@ -182,25 +244,29 @@ Run these commands from \`apps/extension\`. Anhedral generates only \`.env.examp
|
|
|
182
244
|
|
|
183
245
|
Run \`pnpm build\`, then load \`.output/chrome-mv3\` as an unpacked extension from \`chrome://extensions\`.
|
|
184
246
|
|
|
185
|
-
|
|
247
|
+
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.
|
|
186
248
|
`);
|
|
187
249
|
}
|
|
188
250
|
function writeBackground(dir) {
|
|
189
|
-
writeFile(path.join(dir, 'src/entrypoints/background.ts'), `
|
|
251
|
+
writeFile(path.join(dir, 'src/entrypoints/background.ts'), `type ChromeWithSidePanel = typeof chrome & {
|
|
252
|
+
sidePanel: {
|
|
253
|
+
setPanelBehavior: (behavior: { openPanelOnActionClick: boolean }) => Promise<void>;
|
|
254
|
+
};
|
|
255
|
+
};
|
|
190
256
|
|
|
191
257
|
export default defineBackground(() => {
|
|
192
258
|
|
|
193
259
|
// Open the side panel when the extension icon is clicked.
|
|
194
|
-
|
|
260
|
+
(chrome as ChromeWithSidePanel).sidePanel
|
|
195
261
|
.setPanelBehavior({ openPanelOnActionClick: true })
|
|
196
262
|
.catch(() => {});
|
|
197
263
|
});
|
|
198
264
|
`);
|
|
199
265
|
}
|
|
200
|
-
function
|
|
201
|
-
writeFile(path.join(dir,
|
|
266
|
+
function writeSidepanelEntry(dir) {
|
|
267
|
+
writeFile(path.join(dir, 'src/entrypoints/sidepanel/main.tsx'), `import * as React from 'react';
|
|
202
268
|
import { createRoot } from 'react-dom/client';
|
|
203
|
-
import {
|
|
269
|
+
import { SidePanelApp } from './app';
|
|
204
270
|
import '../../styles/main.css';
|
|
205
271
|
|
|
206
272
|
const container = document.getElementById('root');
|
|
@@ -208,14 +274,14 @@ if (container) {
|
|
|
208
274
|
const root = createRoot(container);
|
|
209
275
|
root.render(
|
|
210
276
|
<React.StrictMode>
|
|
211
|
-
<
|
|
277
|
+
<SidePanelApp />
|
|
212
278
|
</React.StrictMode>
|
|
213
279
|
);
|
|
214
280
|
}
|
|
215
281
|
`);
|
|
216
282
|
}
|
|
217
|
-
function
|
|
218
|
-
writeFile(path.join(dir,
|
|
283
|
+
function writeSidepanelHtml(dir, displayName) {
|
|
284
|
+
writeFile(path.join(dir, 'src/entrypoints/sidepanel/index.html'), `<!DOCTYPE html>
|
|
219
285
|
<html lang="en">
|
|
220
286
|
<head>
|
|
221
287
|
<meta charset="UTF-8" />
|
|
@@ -229,9 +295,8 @@ function writeSurfaceHtml(dir, displayName, surface) {
|
|
|
229
295
|
</html>
|
|
230
296
|
`);
|
|
231
297
|
}
|
|
232
|
-
function
|
|
233
|
-
writeFile(path.join(dir,
|
|
234
|
-
import { browser } from 'wxt/browser';
|
|
298
|
+
function writeSidepanelApp(dir) {
|
|
299
|
+
writeFile(path.join(dir, 'src/entrypoints/sidepanel/app.tsx'), `import * as React from 'react';
|
|
235
300
|
import { Button } from '../../components/ui/button';
|
|
236
301
|
|
|
237
302
|
type PageSnapshot = {
|
|
@@ -240,21 +305,17 @@ type PageSnapshot = {
|
|
|
240
305
|
};
|
|
241
306
|
|
|
242
307
|
async function readPageSnapshot(): Promise<PageSnapshot> {
|
|
243
|
-
const [tab] = await
|
|
308
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
244
309
|
if (!tab?.id) throw new Error('No active tab is available.');
|
|
245
|
-
const [
|
|
310
|
+
const [{ result }] = await chrome.scripting.executeScript({
|
|
246
311
|
target: { tabId: tab.id },
|
|
247
312
|
func: () => ({ title: document.title, location: window.location.href }),
|
|
248
313
|
});
|
|
249
|
-
return requireSnapshot(injection?.result);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function requireSnapshot(result: PageSnapshot | undefined): PageSnapshot {
|
|
253
314
|
if (!result) throw new Error('The active page did not return a snapshot.');
|
|
254
|
-
return result;
|
|
315
|
+
return result as PageSnapshot;
|
|
255
316
|
}
|
|
256
317
|
|
|
257
|
-
export function
|
|
318
|
+
export function SidePanelApp() {
|
|
258
319
|
const [page, setPage] = React.useState<PageSnapshot | null>(null);
|
|
259
320
|
const [pageError, setPageError] = React.useState<string | null>(null);
|
|
260
321
|
|
|
@@ -269,7 +330,7 @@ export function ExtensionApp() {
|
|
|
269
330
|
|
|
270
331
|
|
|
271
332
|
return (
|
|
272
|
-
<div style={{ padding: 24
|
|
333
|
+
<div style={{ padding: 24 }}>
|
|
273
334
|
<h2>Welcome!</h2>
|
|
274
335
|
<Button type="button" onClick={() => void readActivePage()}>Read active page</Button>
|
|
275
336
|
{page ? (
|
|
@@ -285,32 +346,9 @@ export function ExtensionApp() {
|
|
|
285
346
|
`);
|
|
286
347
|
}
|
|
287
348
|
function writeStyles(dir) {
|
|
288
|
-
writeFile(path.join(dir, 'src/styles/main.css'), `@
|
|
289
|
-
|
|
290
|
-
@
|
|
291
|
-
--color-background: hsl(var(--background));
|
|
292
|
-
--color-foreground: hsl(var(--foreground));
|
|
293
|
-
--color-card: hsl(var(--card));
|
|
294
|
-
--color-card-foreground: hsl(var(--card-foreground));
|
|
295
|
-
--color-popover: hsl(var(--popover));
|
|
296
|
-
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
297
|
-
--color-primary: hsl(var(--primary));
|
|
298
|
-
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
299
|
-
--color-secondary: hsl(var(--secondary));
|
|
300
|
-
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
301
|
-
--color-muted: hsl(var(--muted));
|
|
302
|
-
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
303
|
-
--color-accent: hsl(var(--accent));
|
|
304
|
-
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
305
|
-
--color-destructive: hsl(var(--destructive));
|
|
306
|
-
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
307
|
-
--color-border: hsl(var(--border));
|
|
308
|
-
--color-input: hsl(var(--input));
|
|
309
|
-
--color-ring: hsl(var(--ring));
|
|
310
|
-
--radius-lg: var(--radius);
|
|
311
|
-
--radius-md: calc(var(--radius) - 2px);
|
|
312
|
-
--radius-sm: calc(var(--radius) - 4px);
|
|
313
|
-
}
|
|
349
|
+
writeFile(path.join(dir, 'src/styles/main.css'), `@tailwind base;
|
|
350
|
+
@tailwind components;
|
|
351
|
+
@tailwind utilities;
|
|
314
352
|
|
|
315
353
|
@layer base {
|
|
316
354
|
:root {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function
|
|
1
|
+
import type { RecipeContext } from '../registry/types.js';
|
|
2
|
+
export declare function scaffoldExpo({ root, options }: RecipeContext): Promise<void>;
|
package/dist/platforms/mobile.js
CHANGED
|
@@ -118,9 +118,7 @@ module.exports = {
|
|
|
118
118
|
}
|
|
119
119
|
function expoScheme(projectName) {
|
|
120
120
|
const unscoped = projectName.replace(/^@[^/]+\//, '');
|
|
121
|
-
const safe = unscoped
|
|
122
|
-
.toLowerCase()
|
|
123
|
-
.replace(/[^a-z0-9+.-]+/g, '-');
|
|
121
|
+
const safe = unscoped.toLowerCase().replace(/[^a-z0-9+.-]+/g, '-');
|
|
124
122
|
let start = 0;
|
|
125
123
|
let end = safe.length;
|
|
126
124
|
while (start < end && '+.-'.includes(safe[start]))
|
|
@@ -131,9 +129,10 @@ function expoScheme(projectName) {
|
|
|
131
129
|
const candidate = normalized || 'app';
|
|
132
130
|
return /^[a-z]/.test(candidate) ? candidate : `app-${candidate}`;
|
|
133
131
|
}
|
|
134
|
-
export async function
|
|
132
|
+
export async function scaffoldExpo({ root, options }) {
|
|
135
133
|
const dir = path.join(root, 'apps/mobile');
|
|
136
|
-
const
|
|
134
|
+
const projectName = options.name;
|
|
135
|
+
const displayName = options.name;
|
|
137
136
|
const nameLiteral = jsString(displayName);
|
|
138
137
|
writeReactLintConfig(dir);
|
|
139
138
|
writeFile(path.join(dir, 'package.json'), JSON.stringify({
|
|
@@ -158,8 +157,15 @@ export async function scaffoldMobile(root, options) {
|
|
|
158
157
|
typecheck: 'tsc --noEmit',
|
|
159
158
|
lint: 'eslint app components lib --max-warnings 0',
|
|
160
159
|
},
|
|
161
|
-
dependencies: {
|
|
162
|
-
|
|
160
|
+
dependencies: {
|
|
161
|
+
...MOBILE_APP_DEPENDENCIES.dependencies,
|
|
162
|
+
...MOBILE_NATIVEWIND_DEPENDENCIES.dependencies,
|
|
163
|
+
},
|
|
164
|
+
devDependencies: {
|
|
165
|
+
...MOBILE_APP_DEPENDENCIES.devDependencies,
|
|
166
|
+
'@workspace/eslint-config': 'workspace:*',
|
|
167
|
+
eslint: '10.0.3',
|
|
168
|
+
},
|
|
163
169
|
}, null, 2) + '\n');
|
|
164
170
|
writeFile(path.join(dir, 'app.json'), JSON.stringify({
|
|
165
171
|
expo: {
|
package/dist/pnpm-policy.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
/** Preserve unrelated starter YAML while replacing the settings this initializer owns. */
|
|
2
2
|
export declare function setPnpmPolicy(root: string, policy: Record<string, unknown>): void;
|
|
3
|
-
/** Only reads the JSON-compatible values written above, never arbitrary client YAML. */
|
|
4
|
-
export declare function generatedPnpmPolicy(root: string, key: string): Record<string, string>;
|
|
5
3
|
/** Merge reviewed patches without deleting starter-maintained mappings. */
|
|
6
4
|
export declare function mergePnpmPatches(root: string, patches: Record<string, string>): void;
|
package/dist/pnpm-policy.js
CHANGED
|
@@ -17,12 +17,6 @@ export function setPnpmPolicy(root, policy) {
|
|
|
17
17
|
}
|
|
18
18
|
writeFile(file, source);
|
|
19
19
|
}
|
|
20
|
-
/** Only reads the JSON-compatible values written above, never arbitrary client YAML. */
|
|
21
|
-
export function generatedPnpmPolicy(root, key) {
|
|
22
|
-
const source = readFileSync(path.join(root, 'pnpm-workspace.yaml'), 'utf8');
|
|
23
|
-
const value = source.match(new RegExp(`^${key}: (.+)$`, 'm'))?.[1];
|
|
24
|
-
return value ? JSON.parse(value) : {};
|
|
25
|
-
}
|
|
26
20
|
/** Merge reviewed patches without deleting starter-maintained mappings. */
|
|
27
21
|
export function mergePnpmPatches(root, patches) {
|
|
28
22
|
const file = path.join(root, 'pnpm-workspace.yaml');
|
|
@@ -32,8 +26,11 @@ export function mergePnpmPatches(root, patches) {
|
|
|
32
26
|
throw new Error('Duplicate patchedDependencies configuration');
|
|
33
27
|
const block = blocks[0]?.[0] ?? '';
|
|
34
28
|
const existing = patchRows(block, patches);
|
|
35
|
-
const replacement = 'patchedDependencies:\n' +
|
|
36
|
-
|
|
29
|
+
const replacement = 'patchedDependencies:\n' +
|
|
30
|
+
existing +
|
|
31
|
+
Object.entries(patches)
|
|
32
|
+
.map(([key, value]) => ` ${JSON.stringify(key)}: ${JSON.stringify(value)}\n`)
|
|
33
|
+
.join('');
|
|
37
34
|
const remaining = source.replace(policyBlock('patchedDependencies'), '');
|
|
38
35
|
writeFile(file, remaining + `${remaining.endsWith('\n') || !remaining ? '' : '\n'}${replacement}`);
|
|
39
36
|
}
|
|
@@ -44,18 +41,26 @@ function patchRows(block, patches) {
|
|
|
44
41
|
const value = header.slice(header.indexOf(':') + 1).trim();
|
|
45
42
|
if (value && !value.startsWith('#')) {
|
|
46
43
|
const object = JSON.parse(value);
|
|
47
|
-
if (!object ||
|
|
44
|
+
if (!object ||
|
|
45
|
+
Array.isArray(object) ||
|
|
46
|
+
typeof object !== 'object' ||
|
|
48
47
|
Object.values(object).some((entry) => typeof entry !== 'string'))
|
|
49
48
|
throw new Error('Unsupported patchedDependencies mapping');
|
|
50
|
-
return Object.entries(object)
|
|
51
|
-
.
|
|
49
|
+
return Object.entries(object)
|
|
50
|
+
.filter(([key]) => !Object.hasOwn(patches, key))
|
|
51
|
+
.map(([key, entry]) => ` ${JSON.stringify(key)}: ${JSON.stringify(entry)}\n`)
|
|
52
|
+
.join('');
|
|
52
53
|
}
|
|
53
|
-
return rows
|
|
54
|
+
return rows
|
|
55
|
+
.filter((row) => {
|
|
54
56
|
if (!row.trim() || row.trimStart().startsWith('#'))
|
|
55
57
|
return true;
|
|
56
58
|
const match = row.match(/^ (?:"([^"\\]+)"|'([^']+)'|([^\s:'"]+)):\s*(?:"[^"\\]+"|'[^']+'|[^\s{}[\]&*!|>]+)(?:\s+#.*)?\s*$/);
|
|
57
59
|
if (!match)
|
|
58
60
|
throw new Error('Unsupported patchedDependencies mapping');
|
|
59
61
|
return !Object.hasOwn(patches, match[1] ?? match[2] ?? match[3]);
|
|
60
|
-
})
|
|
62
|
+
})
|
|
63
|
+
.filter((row, index, rows) => row || index < rows.length - 1)
|
|
64
|
+
.map((row) => `${row}\n`)
|
|
65
|
+
.join('');
|
|
61
66
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface DeploymentTarget {
|
|
2
|
+
id: string;
|
|
3
|
+
directory: string;
|
|
4
|
+
workerName: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ApplicationProject {
|
|
7
|
+
schemaVersion: 1;
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
ownership: 'customer';
|
|
11
|
+
provider: 'cloudflare';
|
|
12
|
+
accountId: string | null;
|
|
13
|
+
audience: 'unconfigured' | 'public' | 'private';
|
|
14
|
+
access?: {
|
|
15
|
+
emails: string[];
|
|
16
|
+
};
|
|
17
|
+
targets: DeploymentTarget[];
|
|
18
|
+
}
|
|
19
|
+
export type DeliveryRunner = (command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv) => Promise<string>;
|
|
20
|
+
export declare function atomicJson(file: string, value: unknown): void;
|
|
21
|
+
export declare function readConfig(directory: string): Record<string, unknown>;
|
|
22
|
+
export declare function targetDirectory(root: string, target: DeploymentTarget): string;
|
|
23
|
+
export declare function registerApplication(root: string, extendTargets?: boolean): ApplicationProject;
|
|
24
|
+
export declare function readApplication(root: string): ApplicationProject;
|
|
25
|
+
export declare const defaultRunner: DeliveryRunner;
|
|
26
|
+
export declare function environment(project: ApplicationProject): NodeJS.ProcessEnv;
|
|
27
|
+
export declare function authenticate(root: string, project: ApplicationProject, run: DeliveryRunner): Promise<void>;
|
|
28
|
+
export declare function withApplicationLock<T>(root: string, operation: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parse } from 'jsonc-parser';
|
|
6
|
+
import { resolveCommand } from './command.js';
|
|
7
|
+
const manifestName = 'anhedral.project.json';
|
|
8
|
+
const stateDirectory = '.anhedral';
|
|
9
|
+
const accountPattern = /^[a-f0-9]{32}$/i;
|
|
10
|
+
const workerPattern = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
11
|
+
export function atomicJson(file, value) {
|
|
12
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
13
|
+
try {
|
|
14
|
+
writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
|
|
15
|
+
renameSync(temporary, file);
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
rmSync(temporary, { force: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function readConfig(directory) {
|
|
22
|
+
const errors = [];
|
|
23
|
+
const value = parse(readFileSync(path.join(directory, 'wrangler.jsonc'), 'utf8'), errors);
|
|
24
|
+
if (errors.length || !value || typeof value !== 'object' || Array.isArray(value))
|
|
25
|
+
throw new Error('Invalid Wrangler JSONC configuration.');
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
export function targetDirectory(root, target) {
|
|
29
|
+
if (!/^apps\/[a-z0-9-]+$/.test(target.directory))
|
|
30
|
+
throw new Error('Deployment targets must be direct apps/* directories.');
|
|
31
|
+
const directory = realpathSync(path.join(root, target.directory));
|
|
32
|
+
if (!directory.startsWith(realpathSync(root) + path.sep))
|
|
33
|
+
throw new Error('Deployment target escapes the project.');
|
|
34
|
+
return directory;
|
|
35
|
+
}
|
|
36
|
+
export function registerApplication(root, extendTargets = false) {
|
|
37
|
+
root = realpathSync(root);
|
|
38
|
+
const existing = existsSync(path.join(root, manifestName)) ? readApplication(root) : undefined;
|
|
39
|
+
if (existing && !extendTargets)
|
|
40
|
+
return existing;
|
|
41
|
+
const setup = JSON.parse(readFileSync(path.join(root, 'anhedral.setup.json'), 'utf8'));
|
|
42
|
+
if (!['cloudflare', 'vercel'].includes(setup.hosting))
|
|
43
|
+
throw new Error('Automatic delivery currently supports Cloudflare projects.');
|
|
44
|
+
const targets = readdirSync(path.join(root, 'apps'), { withFileTypes: true })
|
|
45
|
+
.filter((entry) => entry.isDirectory())
|
|
46
|
+
.filter((entry) => existsSync(path.join(root, 'apps', entry.name, 'wrangler.jsonc')))
|
|
47
|
+
.map((entry) => {
|
|
48
|
+
const workerName = readConfig(path.join(root, 'apps', entry.name)).name;
|
|
49
|
+
if (typeof workerName !== 'string' || !workerPattern.test(workerName))
|
|
50
|
+
throw new Error('Invalid Worker name.');
|
|
51
|
+
return { id: entry.name, directory: `apps/${entry.name}`, workerName };
|
|
52
|
+
})
|
|
53
|
+
.sort((a, b) => (a.id === 'web' ? 1 : b.id === 'web' ? -1 : a.id.localeCompare(b.id)));
|
|
54
|
+
if (existing) {
|
|
55
|
+
if (existing.targets.some((target) => !targets.some((candidate) => candidate.id === target.id &&
|
|
56
|
+
candidate.directory === target.directory &&
|
|
57
|
+
candidate.workerName === target.workerName)))
|
|
58
|
+
throw new Error('Addition cannot replace existing delivery targets.');
|
|
59
|
+
const extended = { ...existing, targets };
|
|
60
|
+
atomicJson(path.join(root, manifestName), extended);
|
|
61
|
+
return extended;
|
|
62
|
+
}
|
|
63
|
+
const project = {
|
|
64
|
+
schemaVersion: 1,
|
|
65
|
+
id: randomUUID(),
|
|
66
|
+
name: JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')).name ?? path.basename(root),
|
|
67
|
+
ownership: 'customer',
|
|
68
|
+
provider: 'cloudflare',
|
|
69
|
+
accountId: null,
|
|
70
|
+
audience: 'unconfigured',
|
|
71
|
+
targets,
|
|
72
|
+
};
|
|
73
|
+
const ignoreFile = path.join(root, '.gitignore');
|
|
74
|
+
const ignored = existsSync(ignoreFile) ? readFileSync(ignoreFile, 'utf8') : '';
|
|
75
|
+
if (!ignored.split(/\r?\n/).includes('.anhedral/'))
|
|
76
|
+
writeFileSync(ignoreFile, ignored + (ignored.endsWith('\n') || !ignored ? '' : '\n') + '.anhedral/\n');
|
|
77
|
+
atomicJson(path.join(root, manifestName), project);
|
|
78
|
+
return project;
|
|
79
|
+
}
|
|
80
|
+
export function readApplication(root) {
|
|
81
|
+
const project = JSON.parse(readFileSync(path.join(root, manifestName), 'utf8'));
|
|
82
|
+
if (project.schemaVersion !== 1 ||
|
|
83
|
+
typeof project.id !== 'string' ||
|
|
84
|
+
!/^[a-f0-9-]{36}$/i.test(project.id) ||
|
|
85
|
+
project.ownership !== 'customer' ||
|
|
86
|
+
project.provider !== 'cloudflare' ||
|
|
87
|
+
!(project.accountId === null || accountPattern.test(project.accountId)) ||
|
|
88
|
+
!['unconfigured', 'public', 'private'].includes(project.audience) ||
|
|
89
|
+
!Array.isArray(project.targets))
|
|
90
|
+
throw new Error('Invalid application manifest.');
|
|
91
|
+
if (project.access &&
|
|
92
|
+
(!Array.isArray(project.access.emails) ||
|
|
93
|
+
project.access.emails.some((email) => typeof email !== 'string')))
|
|
94
|
+
throw new Error('Invalid private audience configuration.');
|
|
95
|
+
if (project.audience === 'private' && !project.access?.emails.length)
|
|
96
|
+
throw new Error('Private delivery requires explicit allowed members.');
|
|
97
|
+
const ids = new Set();
|
|
98
|
+
const workers = new Set();
|
|
99
|
+
for (const target of project.targets) {
|
|
100
|
+
if (!target ||
|
|
101
|
+
typeof target.id !== 'string' ||
|
|
102
|
+
typeof target.workerName !== 'string' ||
|
|
103
|
+
!/^[a-z0-9-]+$/.test(target.id) ||
|
|
104
|
+
ids.has(target.id) ||
|
|
105
|
+
workers.has(target.workerName) ||
|
|
106
|
+
!workerPattern.test(target.workerName))
|
|
107
|
+
throw new Error('Invalid or duplicate deployment target.');
|
|
108
|
+
ids.add(target.id);
|
|
109
|
+
workers.add(target.workerName);
|
|
110
|
+
targetDirectory(root, target);
|
|
111
|
+
}
|
|
112
|
+
return project;
|
|
113
|
+
}
|
|
114
|
+
export const defaultRunner = (command, args, cwd, env) => new Promise((resolve, reject) => {
|
|
115
|
+
const invocation = resolveCommand(command, args);
|
|
116
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
117
|
+
cwd,
|
|
118
|
+
env,
|
|
119
|
+
detached: process.platform !== 'win32',
|
|
120
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
121
|
+
});
|
|
122
|
+
let output = '';
|
|
123
|
+
let oversized = false;
|
|
124
|
+
let interrupted = false;
|
|
125
|
+
let timedOut = false;
|
|
126
|
+
let forceTimer;
|
|
127
|
+
const kill = (signal) => {
|
|
128
|
+
if (!child.pid)
|
|
129
|
+
return;
|
|
130
|
+
if (process.platform === 'win32')
|
|
131
|
+
spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' });
|
|
132
|
+
else {
|
|
133
|
+
try {
|
|
134
|
+
process.kill(-child.pid, signal);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
/* The process group already exited. */
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const stop = () => {
|
|
142
|
+
kill('SIGTERM');
|
|
143
|
+
forceTimer ??= setTimeout(() => kill('SIGKILL'), 2000);
|
|
144
|
+
forceTimer.unref();
|
|
145
|
+
};
|
|
146
|
+
const interrupt = () => {
|
|
147
|
+
interrupted = true;
|
|
148
|
+
stop();
|
|
149
|
+
};
|
|
150
|
+
process.once('SIGINT', interrupt);
|
|
151
|
+
process.once('SIGTERM', interrupt);
|
|
152
|
+
child.stdout.on('data', (chunk) => {
|
|
153
|
+
if (!oversized)
|
|
154
|
+
output += chunk.toString();
|
|
155
|
+
if (output.length > 8 * 1024 * 1024) {
|
|
156
|
+
oversized = true;
|
|
157
|
+
stop();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// Do not persist raw build/provider output: it can contain credentials or customer data.
|
|
161
|
+
child.stderr.resume();
|
|
162
|
+
const timer = setTimeout(() => {
|
|
163
|
+
timedOut = true;
|
|
164
|
+
stop();
|
|
165
|
+
}, 20 * 60 * 1000);
|
|
166
|
+
const clean = () => {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
if (forceTimer)
|
|
169
|
+
clearTimeout(forceTimer);
|
|
170
|
+
process.removeListener('SIGINT', interrupt);
|
|
171
|
+
process.removeListener('SIGTERM', interrupt);
|
|
172
|
+
};
|
|
173
|
+
child.on('error', () => {
|
|
174
|
+
clean();
|
|
175
|
+
reject(new Error(`Cannot run ${command}; check local tooling.`));
|
|
176
|
+
});
|
|
177
|
+
child.on('close', (code) => {
|
|
178
|
+
clean();
|
|
179
|
+
if (code === 0 && !oversized && !interrupted && !timedOut)
|
|
180
|
+
resolve(output);
|
|
181
|
+
else
|
|
182
|
+
reject(new Error(`${command} ${args.slice(0, 3).join(' ')} ${interrupted ? 'was interrupted' : timedOut ? 'timed out' : 'failed'}; inspect the project command locally.`));
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
export function environment(project) {
|
|
186
|
+
if (!project.accountId)
|
|
187
|
+
throw new Error('Connect the intended Cloudflare account first.');
|
|
188
|
+
if (process.env.CLOUDFLARE_ACCOUNT_ID && process.env.CLOUDFLARE_ACCOUNT_ID !== project.accountId)
|
|
189
|
+
throw new Error('Cloudflare environment account conflicts with the registered account.');
|
|
190
|
+
if (process.env.CLOUDFLARE_ENV)
|
|
191
|
+
throw new Error('Register a separate project configuration for the intended environment; CLOUDFLARE_ENV overrides are unsupported.');
|
|
192
|
+
return {
|
|
193
|
+
...process.env,
|
|
194
|
+
CLOUDFLARE_ACCOUNT_ID: project.accountId,
|
|
195
|
+
CI: 'true',
|
|
196
|
+
NO_COLOR: '1',
|
|
197
|
+
FORCE_COLOR: '0',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export async function authenticate(root, project, run) {
|
|
201
|
+
const identity = JSON.parse(await run('pnpm', ['exec', 'wrangler', 'whoami', '--json'], targetDirectory(root, project.targets[0]), environment(project)));
|
|
202
|
+
if (identity.loggedIn !== true ||
|
|
203
|
+
!Array.isArray(identity.accounts) ||
|
|
204
|
+
!identity.accounts.some((account) => account.id === project.accountId))
|
|
205
|
+
throw new Error('Wrangler cannot access the registered Cloudflare account. Run wrangler login or configure scoped credentials.');
|
|
206
|
+
}
|
|
207
|
+
export async function withApplicationLock(root, operation) {
|
|
208
|
+
root = realpathSync(root);
|
|
209
|
+
const state = path.join(root, stateDirectory);
|
|
210
|
+
mkdirSync(state, { recursive: true });
|
|
211
|
+
if (realpathSync(state) !== path.join(root, stateDirectory))
|
|
212
|
+
throw new Error('Delivery state directory must not be a symbolic link.');
|
|
213
|
+
const lock = path.join(state, 'ship.lock');
|
|
214
|
+
try {
|
|
215
|
+
mkdirSync(lock);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
throw new Error('Another delivery is running or was interrupted. Inspect provider status and remove .anhedral/ship.lock only after confirming no delivery is active.');
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
return await operation();
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
rmSync(lock, { recursive: true, force: true });
|
|
225
|
+
}
|
|
226
|
+
}
|