handoff-app 2.0.0-0 → 2.0.0-2
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 +39 -17
- package/config/docs/foundations/colors/guidelines.md +9 -0
- package/config/docs/foundations/effects/guidelines.md +9 -0
- package/config/docs/foundations/effects/resources.md +9 -0
- package/config/docs/foundations/grid.md +10 -0
- package/config/docs/foundations/icons/guidelines.md +9 -0
- package/config/docs/foundations/images/guidelines.md +9 -0
- package/config/docs/foundations/images/resources.md +9 -0
- package/config/docs/foundations/images.md +10 -0
- package/config/docs/foundations/logo/resources.md +9 -0
- package/config/docs/foundations/typography/guidelines.md +9 -0
- package/config/docs/foundations/typography/resources.md +9 -0
- package/config/docs/foundations.md +5 -3
- package/dist/app-builder/build.js +89 -46
- package/dist/app-builder/client-config.js +5 -5
- package/dist/app-builder/paths.js +23 -1
- package/dist/app-builder/static-export.js +25 -0
- package/dist/app-builder/vercel-output.js +2 -2
- package/dist/artifacts/index.d.ts +1 -1
- package/dist/artifacts/index.js +4 -1
- package/dist/artifacts/url.d.ts +18 -0
- package/dist/artifacts/url.js +31 -1
- package/dist/cli/create.js +48 -3
- package/dist/commands/checkout/index.d.ts +6 -6
- package/dist/commands/checkout/index.js +8 -12
- package/dist/commands/publish/index.d.ts +6 -5
- package/dist/commands/publish/index.js +8 -11
- package/dist/commands/utils.d.ts +6 -0
- package/dist/commands/utils.js +9 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.js +26 -10
- package/dist/registry/asset-storage/resolve.d.ts +1 -1
- package/dist/registry/asset-storage/resolve.js +1 -1
- package/dist/registry/checkout/index.d.ts +10 -5
- package/dist/registry/checkout/index.js +62 -11
- package/dist/registry/client.d.ts +2 -1
- package/dist/registry/client.js +8 -0
- package/dist/registry/db/client.d.ts +9 -9
- package/dist/registry/db/client.js +7 -7
- package/dist/registry/db/{adapter.d.ts → driver.d.ts} +16 -14
- package/dist/registry/db/{adapter.js → driver.js} +17 -15
- package/dist/registry/db/index.d.ts +1 -1
- package/dist/registry/db/index.js +9 -9
- package/dist/registry/db/migrate.d.ts +1 -1
- package/dist/registry/db/migrate.js +5 -5
- package/dist/registry/publish/index.d.ts +8 -1
- package/dist/registry/publish/index.js +88 -2
- package/dist/registry/transfer.d.ts +13 -0
- package/dist/types/config.d.ts +12 -8
- package/drizzle.config.ts +1 -1
- package/package.json +1 -1
- package/src/app/components/Foundations/DisplayLogo.tsx +8 -2
- package/src/app/lib/docs-api/runtime-config.ts +10 -10
- package/src/app/lib/registry-api/transfer.ts +29 -0
- package/src/app/lib/registry-connection.ts +2 -2
- package/src/app/next.config.mjs +1 -1
- package/src/app/pages/api/registry/transfer/[kind]/index.ts +21 -0
- package/src/app/pages/assets/logos.tsx +4 -1
- package/src/app/pages/foundations/icons/index.tsx +7 -5
- package/src/app/pages/foundations/icons/{[name] → library/[name]}/index.tsx +8 -8
- package/src/app/pages/foundations/logo.tsx +5 -4
- package/src/app-builder/build.ts +75 -55
- package/src/app-builder/client-config.ts +3 -3
- package/src/app-builder/paths.ts +25 -1
- package/src/app-builder/static-export.ts +27 -0
- package/src/app-builder/vercel-output.ts +2 -2
- package/src/artifacts/index.ts +8 -1
- package/src/artifacts/url.ts +32 -0
- package/src/cli/create.ts +58 -3
- package/src/commands/checkout/index.ts +10 -15
- package/src/commands/publish/index.ts +10 -14
- package/src/commands/utils.ts +12 -0
- package/src/index.ts +26 -12
- package/src/registry/asset-storage/resolve.ts +1 -1
- package/src/registry/checkout/index.ts +73 -11
- package/src/registry/client.ts +7 -1
- package/src/registry/db/client.ts +12 -12
- package/src/registry/db/{adapter.ts → driver.ts} +19 -17
- package/src/registry/db/index.ts +4 -4
- package/src/registry/db/migrate.ts +5 -5
- package/src/registry/publish/index.ts +100 -2
- package/src/registry/transfer.ts +14 -0
- package/src/types/config.ts +12 -8
- package/templates/default/README.md.tpl +40 -1
- package/templates/default/gitignore.tpl +18 -3
- package/templates/default/handoff.config.js.tpl +14 -0
- package/templates/default/handoff.config.ts.tpl +14 -0
- package/templates/default/handoff.config.with-examples.js.tpl +14 -0
- package/templates/default/handoff.config.with-examples.ts.tpl +14 -0
- package/templates/default/package.json.tpl +2 -1
- package/templates/default/vercel.json.tpl +4 -0
package/src/cli/create.ts
CHANGED
|
@@ -23,6 +23,8 @@ interface CreateOptions {
|
|
|
23
23
|
figmaAccessToken: string;
|
|
24
24
|
languagePreference: 'typescript' | 'javascript';
|
|
25
25
|
includeExamples: boolean;
|
|
26
|
+
includeVercel: boolean;
|
|
27
|
+
vercelBuildCommand: string;
|
|
26
28
|
[key: string]: string | boolean; // Allow dynamic access for template replacement
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -67,7 +69,8 @@ interface TemplateConfig {
|
|
|
67
69
|
*/
|
|
68
70
|
const getMandatoryTemplates = (
|
|
69
71
|
includeExamples: boolean,
|
|
70
|
-
languagePreference: 'typescript' | 'javascript'
|
|
72
|
+
languagePreference: 'typescript' | 'javascript',
|
|
73
|
+
includeVercel: boolean
|
|
71
74
|
): TemplateConfig[] => {
|
|
72
75
|
const configFormat = languagePreference;
|
|
73
76
|
const configTemplateBaseName = includeExamples ? 'handoff.config.with-examples' : 'handoff.config';
|
|
@@ -89,6 +92,10 @@ const getMandatoryTemplates = (
|
|
|
89
92
|
templates.push({ source: 'tsconfig.json.tpl', destination: 'tsconfig.json' });
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
if (includeVercel) {
|
|
96
|
+
templates.push({ source: 'vercel.json.tpl', destination: 'vercel.json' });
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
return templates;
|
|
93
100
|
};
|
|
94
101
|
|
|
@@ -118,7 +125,8 @@ const renderTemplate = (content: string, data: CreateOptions): string => {
|
|
|
118
125
|
};
|
|
119
126
|
|
|
120
127
|
const getDependenciesToInstall = (options: CreateOptions): { dependencies: string[]; devDependencies: string[] } => {
|
|
121
|
-
|
|
128
|
+
// TODO: switch back to `handoff-app@latest` before the 2.0 stable release.
|
|
129
|
+
const dependencies = ['handoff-app@next'];
|
|
122
130
|
const devDependencies: string[] = [];
|
|
123
131
|
|
|
124
132
|
if (options.includeExamples) {
|
|
@@ -263,6 +271,51 @@ const create = async (): Promise<void> => {
|
|
|
263
271
|
|
|
264
272
|
const languagePreference = languagePreferenceSelection as 'typescript' | 'javascript';
|
|
265
273
|
|
|
274
|
+
const includeVercelSelection = await p.confirm({
|
|
275
|
+
message: 'Add a Vercel deployment config (vercel.json)?',
|
|
276
|
+
initialValue: true,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (p.isCancel(includeVercelSelection)) {
|
|
280
|
+
p.cancel('Project creation cancelled.');
|
|
281
|
+
process.exit(0);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const includeVercel = includeVercelSelection as boolean;
|
|
285
|
+
|
|
286
|
+
// When a Vercel config is scaffolded, choose which deployment its build command produces. Both
|
|
287
|
+
// package to `.vercel/output`; they differ in what is served. Registry is the recommended,
|
|
288
|
+
// pre-selected option; static stays available as the simplest deploy.
|
|
289
|
+
let vercelBuildCommand = 'npm run build -- --target registry --package vercel';
|
|
290
|
+
if (includeVercel) {
|
|
291
|
+
const vercelDeploymentSelection = await p.select({
|
|
292
|
+
message: 'Which deployment should Vercel build?',
|
|
293
|
+
options: [
|
|
294
|
+
{
|
|
295
|
+
value: 'registry',
|
|
296
|
+
label: 'Registry (dynamic)',
|
|
297
|
+
hint: 'Live, database-backed app (needs a Postgres DATABASE_URL). The more capable, actively evolving option — recommended for a shared design system.',
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
value: 'static',
|
|
301
|
+
label: 'Static site',
|
|
302
|
+
hint: 'Prebuilt snapshot served as plain files. The simplest to deploy and run.',
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
initialValue: 'registry',
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (p.isCancel(vercelDeploymentSelection)) {
|
|
309
|
+
p.cancel('Project creation cancelled.');
|
|
310
|
+
process.exit(0);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
vercelBuildCommand =
|
|
314
|
+
vercelDeploymentSelection === 'registry'
|
|
315
|
+
? 'npm run build -- --target registry --package vercel'
|
|
316
|
+
: 'npm run build -- --target static --package vercel';
|
|
317
|
+
}
|
|
318
|
+
|
|
266
319
|
// Get Figma project ID
|
|
267
320
|
p.log.step(chalk.blue('Figma Configuration'));
|
|
268
321
|
const figmaProjectId = await p.text({
|
|
@@ -295,6 +348,8 @@ const create = async (): Promise<void> => {
|
|
|
295
348
|
figmaAccessToken: ((figmaAccessToken as string) || '').trim(),
|
|
296
349
|
languagePreference,
|
|
297
350
|
includeExamples,
|
|
351
|
+
includeVercel,
|
|
352
|
+
vercelBuildCommand,
|
|
298
353
|
};
|
|
299
354
|
|
|
300
355
|
// Create files
|
|
@@ -309,7 +364,7 @@ const create = async (): Promise<void> => {
|
|
|
309
364
|
|
|
310
365
|
// Combine mandatory templates with example templates based on user choice
|
|
311
366
|
const templates = [
|
|
312
|
-
...getMandatoryTemplates(includeExamples, languagePreference),
|
|
367
|
+
...getMandatoryTemplates(includeExamples, languagePreference, includeVercel),
|
|
313
368
|
...(includeExamples ? getExampleTemplates(languagePreference) : []),
|
|
314
369
|
];
|
|
315
370
|
|
|
@@ -2,20 +2,18 @@ import { CommandModule } from 'yargs';
|
|
|
2
2
|
import Handoff from '../../';
|
|
3
3
|
import { Logger } from '../../utils/logger';
|
|
4
4
|
import { SharedArgs } from '../types';
|
|
5
|
-
import { getSharedOptions } from '../utils';
|
|
6
|
-
|
|
7
|
-
/** Entity kinds checkout-able from a connected workspace. */
|
|
8
|
-
const ENTITY_KINDS = ['component', 'pattern', 'page', 'tokens', 'assets'] as const;
|
|
5
|
+
import { ENTITY_WIRE_KIND, getSharedOptions, REGISTRY_ENTITY_KINDS, RegistryEntityKind } from '../utils';
|
|
9
6
|
|
|
10
7
|
export interface CheckoutArgs extends SharedArgs {
|
|
11
|
-
type:
|
|
8
|
+
type: RegistryEntityKind;
|
|
12
9
|
id?: string;
|
|
13
10
|
}
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
|
-
* `handoff-app checkout <
|
|
17
|
-
* into this workspace
|
|
18
|
-
*
|
|
13
|
+
* `handoff-app checkout <components|patterns|pages> [id]` pulls entities from the connected registry
|
|
14
|
+
* into this workspace, writing their source files in standard authoring form and synthesizing local
|
|
15
|
+
* declarations. Pass an `id` to pull that one entity, or omit it to pull every published entity of
|
|
16
|
+
* that kind.
|
|
19
17
|
*
|
|
20
18
|
* `handoff-app checkout tokens [setId]` — pull every published token set (or only the named set) into
|
|
21
19
|
* this workspace: reconstruct `tokens.json` and restore the generated token files.
|
|
@@ -28,16 +26,16 @@ export interface CheckoutArgs extends SharedArgs {
|
|
|
28
26
|
*/
|
|
29
27
|
const command: CommandModule<{}, CheckoutArgs> = {
|
|
30
28
|
command: 'checkout <type> [id]',
|
|
31
|
-
describe: 'Pull
|
|
29
|
+
describe: 'Pull components, patterns, pages, design tokens, or assets from the connected registry into this workspace',
|
|
32
30
|
builder: (yargs) => {
|
|
33
31
|
return getSharedOptions(yargs)
|
|
34
32
|
.positional('type', {
|
|
35
33
|
describe: 'The kind of entity to checkout',
|
|
36
|
-
choices:
|
|
34
|
+
choices: REGISTRY_ENTITY_KINDS,
|
|
37
35
|
type: 'string',
|
|
38
36
|
})
|
|
39
37
|
.positional('id', {
|
|
40
|
-
describe: 'The stable id of the entity (component/pattern/page id, token set id, or asset collection);
|
|
38
|
+
describe: 'The stable id of the entity (component/pattern/page id, token set id, or asset collection); omit to checkout all of that kind',
|
|
41
39
|
type: 'string',
|
|
42
40
|
});
|
|
43
41
|
},
|
|
@@ -52,10 +50,7 @@ const command: CommandModule<{}, CheckoutArgs> = {
|
|
|
52
50
|
await handoff.checkoutAssets(args.id);
|
|
53
51
|
return;
|
|
54
52
|
}
|
|
55
|
-
|
|
56
|
-
throw new Error(`An id is required to checkout a ${args.type} (e.g. "handoff-app checkout ${args.type} <id>").`);
|
|
57
|
-
}
|
|
58
|
-
await handoff.checkout(args.type, args.id);
|
|
53
|
+
await handoff.checkout(ENTITY_WIRE_KIND[args.type], args.id);
|
|
59
54
|
} catch (error) {
|
|
60
55
|
Logger.error(error instanceof Error ? error.message : String(error));
|
|
61
56
|
process.exitCode = 1;
|
|
@@ -2,19 +2,18 @@ import { CommandModule } from 'yargs';
|
|
|
2
2
|
import Handoff from '../../';
|
|
3
3
|
import { Logger } from '../../utils/logger';
|
|
4
4
|
import { SharedArgs } from '../types';
|
|
5
|
-
import { getSharedOptions } from '../utils';
|
|
6
|
-
|
|
7
|
-
/** Entity kinds publishable from a connected workspace. */
|
|
8
|
-
const ENTITY_KINDS = ['component', 'pattern', 'page', 'tokens', 'assets'] as const;
|
|
5
|
+
import { ENTITY_WIRE_KIND, getSharedOptions, REGISTRY_ENTITY_KINDS, RegistryEntityKind } from '../utils';
|
|
9
6
|
|
|
10
7
|
export interface PublishArgs extends SharedArgs {
|
|
11
|
-
type:
|
|
8
|
+
type: RegistryEntityKind;
|
|
12
9
|
id?: string;
|
|
13
10
|
}
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
|
-
* `handoff-app publish <
|
|
17
|
-
*
|
|
13
|
+
* `handoff-app publish <components|patterns|pages> [id]` runs a fresh local build and uploads to the
|
|
14
|
+
* connected registry. Pass an `id` to build and upload that one entity, or omit it to build the kind
|
|
15
|
+
* once and upload every declared entity, skipping any whose content already matches the registry
|
|
16
|
+
* (pass `--force` to re-upload everything).
|
|
18
17
|
*
|
|
19
18
|
* `handoff-app publish tokens [setId]` — fresh token build + upload of every logical token set, or
|
|
20
19
|
* only the named set (`foundation/colors`, `component/button`).
|
|
@@ -25,16 +24,16 @@ export interface PublishArgs extends SharedArgs {
|
|
|
25
24
|
*/
|
|
26
25
|
const command: CommandModule<{}, PublishArgs> = {
|
|
27
26
|
command: 'publish <type> [id]',
|
|
28
|
-
describe: 'Build and publish
|
|
27
|
+
describe: 'Build and publish components, patterns, pages, design tokens, or assets to the connected registry',
|
|
29
28
|
builder: (yargs) => {
|
|
30
29
|
return getSharedOptions(yargs)
|
|
31
30
|
.positional('type', {
|
|
32
31
|
describe: 'The kind of entity to publish',
|
|
33
|
-
choices:
|
|
32
|
+
choices: REGISTRY_ENTITY_KINDS,
|
|
34
33
|
type: 'string',
|
|
35
34
|
})
|
|
36
35
|
.positional('id', {
|
|
37
|
-
describe: 'The stable id of the entity (component/pattern/page id, token set id, or asset collection);
|
|
36
|
+
describe: 'The stable id of the entity (component/pattern/page id, token set id, or asset collection); omit to publish all of that kind',
|
|
38
37
|
type: 'string',
|
|
39
38
|
});
|
|
40
39
|
},
|
|
@@ -49,10 +48,7 @@ const command: CommandModule<{}, PublishArgs> = {
|
|
|
49
48
|
await handoff.publishAssets(args.id);
|
|
50
49
|
return;
|
|
51
50
|
}
|
|
52
|
-
|
|
53
|
-
throw new Error(`An id is required to publish a ${args.type} (e.g. "handoff-app publish ${args.type} <id>").`);
|
|
54
|
-
}
|
|
55
|
-
await handoff.publish(args.type, args.id);
|
|
51
|
+
await handoff.publish(ENTITY_WIRE_KIND[args.type], args.id);
|
|
56
52
|
} catch (error) {
|
|
57
53
|
Logger.error(error instanceof Error ? error.message : String(error));
|
|
58
54
|
process.exitCode = 1;
|
package/src/commands/utils.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { Argv } from 'yargs';
|
|
2
|
+
import type { TransferEntityKind } from '../registry/transfer';
|
|
3
|
+
|
|
4
|
+
/** Plural entity kinds accepted by the `publish`/`checkout` commands. */
|
|
5
|
+
export const REGISTRY_ENTITY_KINDS = ['components', 'patterns', 'pages', 'tokens', 'assets'] as const;
|
|
6
|
+
export type RegistryEntityKind = (typeof REGISTRY_ENTITY_KINDS)[number];
|
|
7
|
+
|
|
8
|
+
/** Map the plural CLI kind to the singular wire kind used by entity publish/checkout. */
|
|
9
|
+
export const ENTITY_WIRE_KIND: Record<'components' | 'patterns' | 'pages', TransferEntityKind> = {
|
|
10
|
+
components: 'component',
|
|
11
|
+
patterns: 'pattern',
|
|
12
|
+
pages: 'page',
|
|
13
|
+
};
|
|
2
14
|
|
|
3
15
|
export const getSharedOptions = (yargs: Argv) => {
|
|
4
16
|
return yargs.options({
|
package/src/index.ts
CHANGED
|
@@ -138,26 +138,40 @@ class Handoff {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
|
-
* Publish
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
141
|
+
* Publish components, patterns, or pages from this connected workspace to the configured remote
|
|
142
|
+
* registry. With `id` it publishes that one entity after a targeted build; without `id` it builds
|
|
143
|
+
* the kind once and publishes every declared entity, skipping ones whose content is unchanged on
|
|
144
|
+
* the registry (`--force` re-uploads all). Pages upload their record and markdown source because
|
|
145
|
+
* they render at runtime. The publish module is loaded lazily so the registry client and build code
|
|
146
|
+
* never enter the docs app bundle.
|
|
145
147
|
*/
|
|
146
|
-
async publish(kind: 'component' | 'pattern' | 'page', id
|
|
148
|
+
async publish(kind: 'component' | 'pattern' | 'page', id?: string): Promise<Handoff> {
|
|
147
149
|
this.preRunner();
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
if (id) {
|
|
151
|
+
const { publishEntity } = await import('./registry/publish');
|
|
152
|
+
await publishEntity(this, kind, id);
|
|
153
|
+
} else {
|
|
154
|
+
const { publishEntities } = await import('./registry/publish');
|
|
155
|
+
await publishEntities(this, kind);
|
|
156
|
+
}
|
|
150
157
|
return this;
|
|
151
158
|
}
|
|
152
159
|
|
|
153
160
|
/**
|
|
154
|
-
* Checkout
|
|
155
|
-
*
|
|
161
|
+
* Checkout components, patterns, or pages from the connected remote registry into this workspace.
|
|
162
|
+
* With `id` it checks out that one entity; without `id` it checks out every published entity of the
|
|
163
|
+
* kind. Overwriting existing local files requires `--force` or an interactive confirmation. The
|
|
164
|
+
* checkout module is loaded lazily so the registry client never enters the docs app bundle.
|
|
156
165
|
*/
|
|
157
|
-
async checkout(kind: 'component' | 'pattern' | 'page', id
|
|
166
|
+
async checkout(kind: 'component' | 'pattern' | 'page', id?: string): Promise<Handoff> {
|
|
158
167
|
this.preRunner();
|
|
159
|
-
|
|
160
|
-
|
|
168
|
+
if (id) {
|
|
169
|
+
const { checkoutEntity } = await import('./registry/checkout');
|
|
170
|
+
await checkoutEntity(this, kind, id);
|
|
171
|
+
} else {
|
|
172
|
+
const { checkoutEntities } = await import('./registry/checkout');
|
|
173
|
+
await checkoutEntities(this, kind);
|
|
174
|
+
}
|
|
161
175
|
return this;
|
|
162
176
|
}
|
|
163
177
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `runtime.registry.assetStorage` selects the active provider for **new** uploads: the built-in
|
|
5
5
|
* `database` inline default, the pre-packaged `vercel-blob` adapter, or a `custom` module. Mirrors
|
|
6
|
-
* {@link import('../db/
|
|
6
|
+
* {@link import('../db/driver')}: only provider selection, module location, non-secret options, and
|
|
7
7
|
* env-var *names* are ever persisted; secret *values* are read from `process.env` at request time.
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Connected-workspace checkout orchestration.
|
|
3
3
|
*
|
|
4
|
-
* `checkout <
|
|
4
|
+
* `checkout <components|patterns> <id>` reads a normalized registry record and its registry-safe
|
|
5
5
|
* source files from the connected registry through the shared registry client, then writes them
|
|
6
6
|
* into the local workspace in standard authoring form. The local **declaration is synthesized
|
|
7
7
|
* locally** in the configured `runtime.workspace.declarationFormat` — declarations are never read
|
|
@@ -20,7 +20,7 @@ import { resolveComponentDeclaration } from '../../config/runtime';
|
|
|
20
20
|
import Handoff from '../../index';
|
|
21
21
|
import type { DeclarationFormat } from '../../types/config';
|
|
22
22
|
import { Logger } from '../../utils/logger';
|
|
23
|
-
import { createRegistryClient, RegistryClientError } from '../client';
|
|
23
|
+
import { createRegistryClient, type RegistryClient, RegistryClientError } from '../client';
|
|
24
24
|
import { resolveRegistryConnection } from '../connection';
|
|
25
25
|
import { isSafePathSegment, isSafeRelativePath, resolvePathWithin } from '../path';
|
|
26
26
|
import type { CheckoutPayload, TransferEntityKind, TransferFile } from '../transfer';
|
|
@@ -530,26 +530,31 @@ const checkoutPage = async (handoff: Handoff, id: string, payload: CheckoutPaylo
|
|
|
530
530
|
};
|
|
531
531
|
|
|
532
532
|
/**
|
|
533
|
-
* Checkout
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
* messaging on any precondition or fetch
|
|
533
|
+
* Checkout one entity through an already-resolved client: precondition checks → fetch the normalized
|
|
534
|
+
* record + source files → resolve the local target → explicit-overwrite guard → write source files
|
|
535
|
+
* and synthesize the local declaration. Shared by the single-id and bulk entry points so both behave
|
|
536
|
+
* identically. Throws {@link CheckoutError} with actionable messaging on any precondition or fetch
|
|
537
|
+
* failure.
|
|
537
538
|
*/
|
|
538
|
-
|
|
539
|
+
const checkoutSingle = async (
|
|
540
|
+
handoff: Handoff,
|
|
541
|
+
kind: TransferEntityKind,
|
|
542
|
+
id: string,
|
|
543
|
+
client: RegistryClient,
|
|
544
|
+
registryUrl: string
|
|
545
|
+
): Promise<void> => {
|
|
539
546
|
const validId = kind === 'page' ? isSafeRelativePath(id) : isSafePathSegment(id);
|
|
540
547
|
if (!validId) {
|
|
541
548
|
throw new CheckoutError(`Cannot checkout ${kind} with unsafe id "${id}".`);
|
|
542
549
|
}
|
|
543
|
-
const connection = resolveConnectionOrThrow(handoff);
|
|
544
550
|
|
|
545
|
-
Logger.info(`Fetching ${kind} "${id}" from ${
|
|
546
|
-
const client = createRegistryClient({ baseUrl: connection.url, accessToken: connection.accessToken });
|
|
551
|
+
Logger.info(`Fetching ${kind} "${id}" from ${registryUrl}…`);
|
|
547
552
|
let payload: CheckoutPayload;
|
|
548
553
|
try {
|
|
549
554
|
payload = await client.checkout(kind, id);
|
|
550
555
|
} catch (error) {
|
|
551
556
|
if (error instanceof RegistryClientError) {
|
|
552
|
-
throw new CheckoutError(describeFetchFailure(error, kind, id,
|
|
557
|
+
throw new CheckoutError(describeFetchFailure(error, kind, id, registryUrl));
|
|
553
558
|
}
|
|
554
559
|
throw error;
|
|
555
560
|
}
|
|
@@ -594,3 +599,60 @@ export const checkoutEntity = async (handoff: Handoff, kind: TransferEntityKind,
|
|
|
594
599
|
`(${payload.files.length} source file(s) + ${declarationFileName}).`
|
|
595
600
|
);
|
|
596
601
|
};
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Checkout a single component, pattern, or page from the connected registry into this workspace.
|
|
605
|
+
* Overwriting existing local files requires `--force` or an interactive confirmation.
|
|
606
|
+
*/
|
|
607
|
+
export const checkoutEntity = async (handoff: Handoff, kind: TransferEntityKind, id: string): Promise<void> => {
|
|
608
|
+
const connection = resolveConnectionOrThrow(handoff);
|
|
609
|
+
const client = createRegistryClient({ baseUrl: connection.url, accessToken: connection.accessToken });
|
|
610
|
+
await checkoutSingle(handoff, kind, id, client, connection.url);
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Checkout every published component, pattern, or page of the kind into this workspace. Enumerates
|
|
615
|
+
* the registry's published ids, then checks each out through the shared client. A per-entity failure
|
|
616
|
+
* is collected and never aborts the rest; the run throws at the end if any entity failed. Overwrite
|
|
617
|
+
* prompting is per entity (skipped under `--force`).
|
|
618
|
+
*/
|
|
619
|
+
export const checkoutEntities = async (handoff: Handoff, kind: TransferEntityKind): Promise<void> => {
|
|
620
|
+
const connection = resolveConnectionOrThrow(handoff);
|
|
621
|
+
const client = createRegistryClient({ baseUrl: connection.url, accessToken: connection.accessToken });
|
|
622
|
+
|
|
623
|
+
let summaries;
|
|
624
|
+
try {
|
|
625
|
+
summaries = await client.listEntities(kind);
|
|
626
|
+
} catch (error) {
|
|
627
|
+
if (error instanceof RegistryClientError) {
|
|
628
|
+
throw new CheckoutError(`Could not list ${kind}s from the registry at ${connection.url}: ${error.message}`);
|
|
629
|
+
}
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (summaries.length === 0) {
|
|
634
|
+
Logger.success(`No ${kind}s are published in the registry; nothing to checkout.`);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
let checkedOut = 0;
|
|
639
|
+
const failed: { id: string; message: string }[] = [];
|
|
640
|
+
for (const { id } of summaries) {
|
|
641
|
+
try {
|
|
642
|
+
await checkoutSingle(handoff, kind, id, client, connection.url);
|
|
643
|
+
checkedOut += 1;
|
|
644
|
+
} catch (error) {
|
|
645
|
+
failed.push({ id, message: error instanceof Error ? error.message : String(error) });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
Logger.success(
|
|
650
|
+
`${kind[0].toUpperCase()}${kind.slice(1)}s checkout complete — ${checkedOut} checked out${failed.length ? `, ${failed.length} failed` : ''}.`
|
|
651
|
+
);
|
|
652
|
+
if (failed.length > 0) {
|
|
653
|
+
for (const failure of failed) {
|
|
654
|
+
Logger.error(` - ${failure.id}: ${failure.message}`);
|
|
655
|
+
}
|
|
656
|
+
throw new CheckoutError(`${failed.length} ${kind}(s) failed to checkout.`);
|
|
657
|
+
}
|
|
658
|
+
};
|
package/src/registry/client.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* and the browser both provide it), so this module stays runtime-agnostic.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type { CheckoutPayload, TransferEntityKind, TransferPackage } from './transfer';
|
|
11
|
+
import type { CheckoutPayload, EntitySummary, TransferEntityKind, TransferPackage } from './transfer';
|
|
12
12
|
import type { TokenSetCheckoutPayload, TokenSetSummary, TokenSetTransferPackage } from './tokens/transfer';
|
|
13
13
|
import type { AssetCollectionCheckoutPayload, AssetCollectionSummary, AssetCollectionTransferPackage } from './assets/transfer';
|
|
14
14
|
|
|
@@ -55,6 +55,7 @@ export interface RegistryClientOptions {
|
|
|
55
55
|
export interface RegistryClient {
|
|
56
56
|
publish(kind: TransferEntityKind, id: string, pkg: TransferPackage): Promise<RegistryEnvelope>;
|
|
57
57
|
checkout(kind: TransferEntityKind, id: string): Promise<CheckoutPayload>;
|
|
58
|
+
listEntities(kind: TransferEntityKind): Promise<EntitySummary[]>;
|
|
58
59
|
listTokenSets(): Promise<TokenSetSummary[]>;
|
|
59
60
|
publishTokens(pkg: TokenSetTransferPackage): Promise<RegistryEnvelope>;
|
|
60
61
|
checkoutTokens(id: string): Promise<TokenSetCheckoutPayload>;
|
|
@@ -154,6 +155,11 @@ export const createRegistryClient = ({ baseUrl, accessToken }: RegistryClientOpt
|
|
|
154
155
|
}
|
|
155
156
|
return envelope.data;
|
|
156
157
|
},
|
|
158
|
+
/** List the registry's entities of one kind (id + build hashes) for skip-unchanged / bulk checkout. */
|
|
159
|
+
async listEntities(kind: TransferEntityKind): Promise<EntitySummary[]> {
|
|
160
|
+
const envelope = await request<{ entities: EntitySummary[] }>('GET', `/api/registry/transfer/${kind}`);
|
|
161
|
+
return envelope.data?.entities ?? [];
|
|
162
|
+
},
|
|
157
163
|
/** List the registry's token sets (id + kind + source hash) for skip-unchanged / bulk checkout. */
|
|
158
164
|
async listTokenSets(): Promise<TokenSetSummary[]> {
|
|
159
165
|
const envelope = await request<{ sets: TokenSetSummary[] }>('GET', '/api/registry/transfer/tokens');
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bundled Drizzle client + migrator for the registry database.
|
|
3
3
|
*
|
|
4
|
-
* The package ships both built-in
|
|
4
|
+
* The package ships both built-in drivers (`pg` and `neon`) and the Drizzle client/migrator, so a
|
|
5
5
|
* consuming project needs no global Drizzle install or separate CLI. Driver modules are loaded
|
|
6
|
-
* lazily (dynamic import) for the selected
|
|
6
|
+
* lazily (dynamic import) for the selected driver only, so workspace dev/build never pulls the
|
|
7
7
|
* Postgres/Neon drivers into memory when the registry is not in use.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { NeonDatabase } from 'drizzle-orm/neon-serverless';
|
|
11
11
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
12
|
-
import type {
|
|
12
|
+
import type { RegistryDatabaseDriver } from './driver';
|
|
13
13
|
import * as schema from './schema';
|
|
14
14
|
|
|
15
|
-
/** Typed Drizzle database over the registry schema, regardless of which
|
|
15
|
+
/** Typed Drizzle database over the registry schema, regardless of which driver backs it. */
|
|
16
16
|
export type RegistryDatabase = NodePgDatabase<typeof schema> | NeonDatabase<typeof schema>;
|
|
17
17
|
|
|
18
18
|
/** A live registry database connection plus its lifecycle operations. */
|
|
19
19
|
export interface RegistryDbConnection {
|
|
20
|
-
/** Selected
|
|
21
|
-
|
|
20
|
+
/** Selected driver that produced this connection. */
|
|
21
|
+
driver: RegistryDatabaseDriver;
|
|
22
22
|
/** Typed Drizzle database. */
|
|
23
23
|
db: RegistryDatabase;
|
|
24
24
|
/** Apply the package-owned migrations found in `migrationsFolder`. */
|
|
@@ -28,20 +28,20 @@ export interface RegistryDbConnection {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export interface CreateRegistryDbConnectionParams {
|
|
31
|
-
|
|
31
|
+
driver: RegistryDatabaseDriver;
|
|
32
32
|
connectionString: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Create a registry database connection for the resolved
|
|
36
|
+
* Create a registry database connection for the resolved driver. Both drivers target the
|
|
37
37
|
* Postgres dialect and share the same schema, so the returned connection is interchangeable for
|
|
38
38
|
* migration and (later) the registry store.
|
|
39
39
|
*/
|
|
40
40
|
export const createRegistryDbConnection = async ({
|
|
41
|
-
|
|
41
|
+
driver,
|
|
42
42
|
connectionString,
|
|
43
43
|
}: CreateRegistryDbConnectionParams): Promise<RegistryDbConnection> => {
|
|
44
|
-
if (
|
|
44
|
+
if (driver === 'neon') {
|
|
45
45
|
const { Pool, neonConfig } = await import('@neondatabase/serverless');
|
|
46
46
|
// Neon's serverless driver needs a WebSocket constructor in Node; reuse the bundled `ws`.
|
|
47
47
|
const ws = await import('ws');
|
|
@@ -59,7 +59,7 @@ export const createRegistryDbConnection = async ({
|
|
|
59
59
|
const db = drizzle(pool, { schema });
|
|
60
60
|
|
|
61
61
|
return {
|
|
62
|
-
|
|
62
|
+
driver,
|
|
63
63
|
db,
|
|
64
64
|
migrate: (migrationsFolder: string) => migrate(db, { migrationsFolder }),
|
|
65
65
|
close: () => pool.end(),
|
|
@@ -80,7 +80,7 @@ export const createRegistryDbConnection = async ({
|
|
|
80
80
|
const db = drizzle(pool, { schema });
|
|
81
81
|
|
|
82
82
|
return {
|
|
83
|
-
|
|
83
|
+
driver,
|
|
84
84
|
db,
|
|
85
85
|
migrate: (migrationsFolder: string) => migrate(db, { migrationsFolder }),
|
|
86
86
|
close: () => pool.end(),
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Registry database
|
|
2
|
+
* Registry database driver resolution.
|
|
3
3
|
*
|
|
4
|
-
* `runtime.registry.database.
|
|
5
|
-
* `handoff-app` package — `pg` (default) and `neon` —
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* PostgreSQL is the supported database. `runtime.registry.database.driver` selects one of two
|
|
5
|
+
* built-in connection drivers shipped with the `handoff-app` package — `pg` (default) and `neon` —
|
|
6
|
+
* both targeting the same Postgres dialect over one package-owned schema and migration set. The
|
|
7
|
+
* driver picks *how* to connect, not *which* database engine to use. `build --target registry` and
|
|
8
|
+
* `db:migrate` resolve the **same** driver so build and migration never diverge. Connection
|
|
9
|
+
* credentials are referenced by environment-variable name only and resolved to a value at runtime;
|
|
10
|
+
* the value is never stored in config.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import type { Config } from '../../types/config';
|
|
12
14
|
|
|
13
|
-
/** Built-in registry database
|
|
14
|
-
export type
|
|
15
|
+
/** Built-in registry database connection driver. Both ship with the package and target Postgres. */
|
|
16
|
+
export type RegistryDatabaseDriver = 'pg' | 'neon';
|
|
15
17
|
|
|
16
18
|
/** Default env-var name holding the database connection string. */
|
|
17
19
|
export const DEFAULT_DATABASE_URL_ENV = 'DATABASE_URL';
|
|
@@ -19,13 +21,13 @@ export const DEFAULT_DATABASE_URL_ENV = 'DATABASE_URL';
|
|
|
19
21
|
/** Default env-var name holding the registry management API bearer token. */
|
|
20
22
|
export const DEFAULT_REGISTRY_API_TOKEN_ENV = 'HANDOFF_REGISTRY_API_TOKEN';
|
|
21
23
|
|
|
22
|
-
/** Default registry database
|
|
23
|
-
export const
|
|
24
|
+
/** Default registry database driver when none is configured. */
|
|
25
|
+
export const DEFAULT_REGISTRY_DRIVER: RegistryDatabaseDriver = 'pg';
|
|
24
26
|
|
|
25
27
|
/** Fully resolved registry database connection inputs. */
|
|
26
28
|
export interface ResolvedRegistryDatabase {
|
|
27
|
-
/** Selected built-in
|
|
28
|
-
|
|
29
|
+
/** Selected built-in driver. */
|
|
30
|
+
driver: RegistryDatabaseDriver;
|
|
29
31
|
/** Name of the env var the connection string was read from. */
|
|
30
32
|
databaseUrlEnv: string;
|
|
31
33
|
/** Resolved connection string (never persisted in config). */
|
|
@@ -48,9 +50,9 @@ export const resolveApiTokenEnv = (config: Config | null | undefined): string =>
|
|
|
48
50
|
return configured || DEFAULT_REGISTRY_API_TOKEN_ENV;
|
|
49
51
|
};
|
|
50
52
|
|
|
51
|
-
/** Resolve the configured registry
|
|
52
|
-
export const
|
|
53
|
-
return config?.runtime?.registry?.database?.
|
|
53
|
+
/** Resolve the configured registry database driver (defaults to `pg`). */
|
|
54
|
+
export const resolveRegistryDriver = (config: Config | null | undefined): RegistryDatabaseDriver => {
|
|
55
|
+
return config?.runtime?.registry?.database?.driver ?? DEFAULT_REGISTRY_DRIVER;
|
|
54
56
|
};
|
|
55
57
|
|
|
56
58
|
/**
|
|
@@ -59,7 +61,7 @@ export const resolveRegistryAdapter = (config: Config | null | undefined): Regis
|
|
|
59
61
|
* message instead of failing deep inside the driver.
|
|
60
62
|
*/
|
|
61
63
|
export const resolveRegistryDatabase = (config: Config | null | undefined): ResolvedRegistryDatabase => {
|
|
62
|
-
const
|
|
64
|
+
const driver = resolveRegistryDriver(config);
|
|
63
65
|
const databaseUrlEnv = resolveDatabaseUrlEnv(config);
|
|
64
66
|
const connectionString = process.env[databaseUrlEnv]?.trim();
|
|
65
67
|
|
|
@@ -70,5 +72,5 @@ export const resolveRegistryDatabase = (config: Config | null | undefined): Reso
|
|
|
70
72
|
);
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
return {
|
|
75
|
+
return { driver, databaseUrlEnv, connectionString };
|
|
74
76
|
};
|
package/src/registry/db/index.ts
CHANGED
|
@@ -15,15 +15,15 @@ export type { ArtifactBuildStatus as RegistryBuildStatus } from '../../artifacts
|
|
|
15
15
|
|
|
16
16
|
export {
|
|
17
17
|
DEFAULT_DATABASE_URL_ENV,
|
|
18
|
-
|
|
18
|
+
DEFAULT_REGISTRY_DRIVER,
|
|
19
19
|
DEFAULT_REGISTRY_API_TOKEN_ENV,
|
|
20
20
|
resolveApiTokenEnv,
|
|
21
21
|
resolveDatabaseUrlEnv,
|
|
22
|
-
|
|
22
|
+
resolveRegistryDriver,
|
|
23
23
|
resolveRegistryDatabase,
|
|
24
|
-
type
|
|
24
|
+
type RegistryDatabaseDriver,
|
|
25
25
|
type ResolvedRegistryDatabase,
|
|
26
|
-
} from './
|
|
26
|
+
} from './driver';
|
|
27
27
|
|
|
28
28
|
export {
|
|
29
29
|
createRegistryDbConnection,
|