handoff-app 2.0.0-7 → 2.0.0-9
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 +130 -14
- package/UPGRADE.md +20 -0
- package/dist/app-builder/build.js +10 -2
- package/dist/app-builder/client-config.js +3 -1
- package/dist/app-builder/watchers.js +17 -7
- package/dist/cache/build-cache.js +5 -1
- package/dist/catalog/normalize.js +3 -0
- package/dist/catalog/previews.js +5 -4
- package/dist/catalog/types.d.ts +8 -0
- package/dist/cli/auth/device.d.ts +6 -1
- package/dist/cli/auth/device.js +5 -2
- package/dist/cli/auth/store.d.ts +15 -6
- package/dist/cli/auth/store.js +72 -19
- package/dist/commands/login/index.d.ts +5 -0
- package/dist/commands/login/index.js +18 -6
- package/dist/commands/logout/index.d.ts +5 -0
- package/dist/commands/logout/index.js +54 -15
- package/dist/commands/types.d.ts +2 -0
- package/dist/commands/utils.d.ts +21 -7
- package/dist/commands/utils.js +11 -10
- package/dist/config/catalog-include.d.ts +2 -2
- package/dist/config/defaults.d.ts +3 -3
- package/dist/config/defaults.js +54 -48
- package/dist/config/env.d.ts +9 -0
- package/dist/config/env.js +43 -0
- package/dist/config/errors.d.ts +4 -0
- package/dist/config/errors.js +11 -0
- package/dist/config/from-env.d.ts +24 -0
- package/dist/config/from-env.js +7 -0
- package/dist/config/helpers.d.ts +2 -0
- package/dist/config/helpers.js +3 -1
- package/dist/config/index.d.ts +2 -1
- package/dist/config/index.js +6 -1
- package/dist/config/loader.d.ts +29 -11
- package/dist/config/loader.js +121 -46
- package/dist/config/resolve-env.d.ts +5 -0
- package/dist/config/resolve-env.js +102 -0
- package/dist/config/runtime.d.ts +3 -3
- package/dist/config/runtime.js +8 -10
- package/dist/config/validator.d.ts +2 -2
- package/dist/config/validator.js +2 -4
- package/dist/handlebars.d.ts +10 -2
- package/dist/index.d.ts +33 -4
- package/dist/index.js +60 -16
- package/dist/pipeline/index.d.ts +1 -1
- package/dist/pipeline/index.js +1 -1
- package/dist/registry/asset-storage/adapters/vercel-blob.js +1 -1
- package/dist/registry/asset-storage/resolve.d.ts +2 -2
- package/dist/registry/asset-storage/resolve.js +4 -1
- package/dist/registry/checkout/index.js +4 -4
- package/dist/registry/connection.d.ts +28 -24
- package/dist/registry/connection.js +53 -67
- package/dist/registry/db/driver.d.ts +4 -11
- package/dist/registry/db/driver.js +4 -16
- package/dist/registry/db/index.d.ts +1 -1
- package/dist/registry/db/index.js +1 -2
- package/dist/registry/publish/index.js +4 -4
- package/dist/store/filesystem.d.ts +2 -2
- package/dist/transformers/plugins/ssr-render.js +40 -15
- package/dist/transformers/preview/component/css.d.ts +1 -0
- package/dist/transformers/preview/component/css.js +85 -47
- package/dist/transformers/preview/pattern/html.js +3 -1
- package/dist/transformers/preview/types.d.ts +13 -0
- package/dist/transformers/utils/build.d.ts +19 -0
- package/dist/transformers/utils/build.js +83 -1
- package/dist/transformers/utils/module.js +2 -1
- package/dist/types/config.d.ts +54 -29
- package/docs/api.md +31 -0
- package/docs/cli.md +2 -2
- package/package.json +1 -1
- package/src/app/components/Component/PageSliceResolver.tsx +1 -0
- package/src/app/components/Component/Preview.tsx +380 -189
- package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
- package/src/app/pages/account/tokens.tsx +3 -1
- package/src/app-builder/build.ts +10 -2
- package/src/app-builder/client-config.ts +5 -3
- package/src/app-builder/watchers.ts +18 -8
- package/src/cache/build-cache.ts +5 -1
- package/src/catalog/README.md +3 -0
- package/src/catalog/normalize.ts +2 -0
- package/src/catalog/previews.ts +6 -3
- package/src/catalog/types.ts +8 -0
- package/src/cli/auth/device.ts +8 -3
- package/src/cli/auth/store.ts +75 -18
- package/src/commands/login/index.ts +20 -6
- package/src/commands/logout/index.ts +74 -20
- package/src/commands/types.ts +2 -0
- package/src/commands/utils.ts +14 -5
- package/src/config/README.md +61 -1
- package/src/config/catalog-include.ts +2 -2
- package/src/config/defaults.ts +24 -14
- package/src/config/env.ts +41 -0
- package/src/config/errors.ts +7 -0
- package/src/config/from-env.ts +26 -0
- package/src/config/helpers.ts +4 -2
- package/src/config/index.ts +4 -1
- package/src/config/loader.ts +158 -50
- package/src/config/resolve-env.ts +94 -0
- package/src/config/runtime.ts +12 -7
- package/src/config/validator.ts +4 -6
- package/src/handlebars.ts +8 -2
- package/src/index.d.ts +2 -2
- package/src/index.ts +85 -8
- package/src/pipeline/index.ts +1 -1
- package/src/registry/asset-storage/adapters/vercel-blob.ts +1 -1
- package/src/registry/asset-storage/resolve.ts +6 -3
- package/src/registry/checkout/index.ts +5 -5
- package/src/registry/connection.ts +72 -71
- package/src/registry/db/driver.ts +6 -17
- package/src/registry/db/index.ts +0 -1
- package/src/registry/publish/index.ts +5 -5
- package/src/store/filesystem.ts +2 -2
- package/src/transformers/plugins/ssr-render.ts +53 -16
- package/src/transformers/preview/component/css.ts +93 -50
- package/src/transformers/preview/pattern/html.ts +3 -1
- package/src/transformers/preview/types.ts +14 -0
- package/src/transformers/utils/build.ts +80 -0
- package/src/transformers/utils/module.ts +4 -2
- package/src/types/config.ts +51 -29
- package/templates/default/env.tpl +10 -0
- package/templates/default/gitignore.tpl +7 -0
- package/templates/default/handoff.config.js.tpl +21 -172
- package/templates/default/handoff.config.ts.tpl +21 -156
- package/templates/default/handoff.config.with-examples.js.tpl +20 -171
- package/templates/default/handoff.config.with-examples.ts.tpl +20 -155
package/README.md
CHANGED
|
@@ -74,17 +74,19 @@ The declaration supplies stable identity, documentation metadata, source entries
|
|
|
74
74
|
|
|
75
75
|
### React
|
|
76
76
|
|
|
77
|
-
Handoff finds the implementation file from its import, so you do not need to repeat the path
|
|
77
|
+
Handoff finds the implementation file from its import, so you do not need to repeat the path, and
|
|
78
|
+
builds the item's CSS from the stylesheets that import chain pulls in, descendants included.
|
|
78
79
|
Previews are named exports. `Preview<typeof item>` gives them the implementation's argument type.
|
|
79
80
|
|
|
80
81
|
```tsx
|
|
81
82
|
// components/example/Component.tsx
|
|
82
83
|
export type ComponentProps = {
|
|
83
84
|
label: string;
|
|
85
|
+
onSelect?: () => void;
|
|
84
86
|
};
|
|
85
87
|
|
|
86
|
-
export default function Component({ label }: ComponentProps) {
|
|
87
|
-
return <div>{label}</div>;
|
|
88
|
+
export default function Component({ label, onSelect }: ComponentProps) {
|
|
89
|
+
return <div onClick={onSelect}>{label}</div>;
|
|
88
90
|
}
|
|
89
91
|
```
|
|
90
92
|
|
|
@@ -112,6 +114,20 @@ export const Default = {
|
|
|
112
114
|
|
|
113
115
|
The export name is the preview name. Use `name` to set a different display title.
|
|
114
116
|
|
|
117
|
+
Arguments are serialized, so a preview that needs an icon, a callback, JSX children, or its own
|
|
118
|
+
state declares `render` instead. It renders as a component, so a capitalized name lets it use hooks, and the item keeps
|
|
119
|
+
documenting the implementation rather than a wrapper. Name a declaration that holds JSX
|
|
120
|
+
`example.handoff.tsx`.
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
export const Interactive = {
|
|
124
|
+
render: function Interactive() {
|
|
125
|
+
const [label, setLabel] = useState('Example');
|
|
126
|
+
return <Component label={label} onSelect={() => setLabel('Selected')} />;
|
|
127
|
+
},
|
|
128
|
+
} satisfies ComponentPreview;
|
|
129
|
+
```
|
|
130
|
+
|
|
115
131
|
### Handlebars
|
|
116
132
|
|
|
117
133
|
The implementation is the path to the template. Arguments are `Record<string, unknown>` by default;
|
|
@@ -359,12 +375,12 @@ runtime variables listed above are supplied through the deployment environment.
|
|
|
359
375
|
|
|
360
376
|
#### Neon PostgreSQL
|
|
361
377
|
|
|
362
|
-
|
|
378
|
+
Import `fromEnv` from `handoff-app`. Select the Neon connection driver in `handoff.config.ts`:
|
|
363
379
|
|
|
364
380
|
```ts
|
|
365
381
|
runtime: {
|
|
366
382
|
registry: {
|
|
367
|
-
|
|
383
|
+
databaseUrl: fromEnv('DATABASE_URL'),
|
|
368
384
|
database: {
|
|
369
385
|
driver: 'neon',
|
|
370
386
|
},
|
|
@@ -404,6 +420,12 @@ should not be left unattended before installation is complete.
|
|
|
404
420
|
|
|
405
421
|
### 4. Workspace authorization
|
|
406
422
|
|
|
423
|
+
A workspace reaches a registry with a registry URL and an access token. There
|
|
424
|
+
are two ways to supply them. Both use the same kind of token, and both are
|
|
425
|
+
listed and revoked under Account → Access tokens in the registry.
|
|
426
|
+
|
|
427
|
+
#### Device login
|
|
428
|
+
|
|
407
429
|
CLI authorization is started from the source workspace with:
|
|
408
430
|
|
|
409
431
|
```bash
|
|
@@ -411,9 +433,48 @@ npm run login -- --url http://localhost:4000
|
|
|
411
433
|
```
|
|
412
434
|
|
|
413
435
|
Registry sign-in, entry of the displayed device code, and CLI approval are
|
|
414
|
-
completed in the browser. The
|
|
436
|
+
completed in the browser. The issued credential is saved in
|
|
415
437
|
`.handoff/cli-auth.json` for that exact registry URL.
|
|
416
438
|
|
|
439
|
+
One credential is saved per profile, so a workspace can stay signed in to
|
|
440
|
+
several registries at the same time:
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
npm run login -- --profile staging --url https://staging.example.com
|
|
444
|
+
npm run publish -- all --profile staging
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
A login profile name is free. It needs no `handoff.config.<name>.*` file, and
|
|
448
|
+
`login` is the only command that accepts a new name. A profile without its own
|
|
449
|
+
login falls back to the default login, so one `npm run login` is still enough
|
|
450
|
+
for a workspace with one registry.
|
|
451
|
+
|
|
452
|
+
`npm run logout` revokes and removes the login of the selected profile. The
|
|
453
|
+
`--all` option removes every saved login.
|
|
454
|
+
|
|
455
|
+
#### Environment variables
|
|
456
|
+
|
|
457
|
+
A token is created in the registry under Account → Access tokens. Read and
|
|
458
|
+
write access is required to publish; read access is enough to check out. The
|
|
459
|
+
token and the registry URL are then set in `.env`:
|
|
460
|
+
|
|
461
|
+
```dotenv
|
|
462
|
+
HANDOFF_REGISTRY_URL=http://localhost:4000
|
|
463
|
+
HANDOFF_REGISTRY_ACCESS_TOKEN=hnd_...
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
No configuration file entry is needed, because these two variables are the
|
|
467
|
+
defaults for `runtime.registryConnection`. A selected profile reads
|
|
468
|
+
`.env.<profile>` on top of `.env`, so one workspace can address a different
|
|
469
|
+
registry per profile. In CI, the job environment supplies the same two
|
|
470
|
+
variables instead of a file.
|
|
471
|
+
|
|
472
|
+
Environment values win over a saved device login, so a CI job stays
|
|
473
|
+
deterministic on a machine where a developer is signed in. A token is only used
|
|
474
|
+
for the registry URL it was issued for. If the environment names a different
|
|
475
|
+
registry than the login of the selected profile, publish and checkout report
|
|
476
|
+
which login was skipped and why.
|
|
477
|
+
|
|
417
478
|
### 5. Content publishing
|
|
418
479
|
|
|
419
480
|
Every kind in the workspace is published in dependency order with:
|
|
@@ -462,13 +523,18 @@ After the registry is reloaded, the published catalog items and foundations are
|
|
|
462
523
|
visible. Published database records are read by registry pages; the local
|
|
463
524
|
workspace is never read directly.
|
|
464
525
|
|
|
465
|
-
For CI,
|
|
526
|
+
For CI, supply `HANDOFF_REGISTRY_URL` and `HANDOFF_REGISTRY_ACCESS_TOKEN`
|
|
527
|
+
through the job environment, as described under
|
|
528
|
+
[Workspace authorization](#4-workspace-authorization). A connection block in
|
|
529
|
+
`handoff.config.ts` is needed only to pin the URL in the repository, or to read
|
|
530
|
+
the values from differently named variables (`fromEnv` is imported from
|
|
531
|
+
`handoff-app`):
|
|
466
532
|
|
|
467
533
|
```ts
|
|
468
534
|
runtime: {
|
|
469
535
|
registryConnection: {
|
|
470
536
|
url: 'https://registry.example.com',
|
|
471
|
-
|
|
537
|
+
accessToken: fromEnv('HANDOFF_REGISTRY_ACCESS_TOKEN'),
|
|
472
538
|
},
|
|
473
539
|
},
|
|
474
540
|
```
|
|
@@ -521,17 +587,19 @@ for Claude Code, Cursor and VS Code behind the plug icon in the header. By hand:
|
|
|
521
587
|
## Configuration
|
|
522
588
|
|
|
523
589
|
Configuration is read from `handoff.config.ts`, `.js`, `.cjs`, or `.json`, in
|
|
524
|
-
that order. `defineConfig` provides typed authoring.
|
|
525
|
-
|
|
590
|
+
that order. `defineConfig` provides typed authoring. Values merge onto the
|
|
591
|
+
defaults: plain objects merge recursively, and arrays, scalars, `null`, and
|
|
592
|
+
functions replace.
|
|
526
593
|
|
|
527
594
|
Useful environment variables:
|
|
528
595
|
|
|
529
596
|
| Variable | Purpose |
|
|
530
597
|
| --- | --- |
|
|
598
|
+
| `HANDOFF_PROFILE` | Config profile merged onto the base config |
|
|
531
599
|
| `HANDOFF_FIGMA_PROJECT_ID` | Figma file ID used by `fetch` |
|
|
532
600
|
| `HANDOFF_DEV_ACCESS_TOKEN` | Figma personal access token used by `fetch` |
|
|
533
601
|
| `HANDOFF_REGISTRY_URL` | Connected workspace registry URL |
|
|
534
|
-
| `HANDOFF_REGISTRY_ACCESS_TOKEN` |
|
|
602
|
+
| `HANDOFF_REGISTRY_ACCESS_TOKEN` | Registry access token used by a connected workspace |
|
|
535
603
|
| `HANDOFF_SYNC_SECRET` | Optional deployment-wide registry credential |
|
|
536
604
|
| `DATABASE_URL` | Registry PostgreSQL connection string |
|
|
537
605
|
| `AUTH_SECRET` | Registry session-signing secret, at least 32 characters |
|
|
@@ -550,6 +618,52 @@ Vercel Blob or a custom adapter can be selected through
|
|
|
550
618
|
selected through `runtime.registry.database.driver`; PostgreSQL is used by
|
|
551
619
|
both.
|
|
552
620
|
|
|
621
|
+
## Profiles
|
|
622
|
+
|
|
623
|
+
A profile is a sidecar config file that merges onto the base config. It holds
|
|
624
|
+
only what changes between environments, so a project keeps one shared config
|
|
625
|
+
instead of a second complete copy of it.
|
|
626
|
+
|
|
627
|
+
- The base config is `handoff.config.ts`, `.js`, `.cjs`, or `.json`. It is the
|
|
628
|
+
only config file loaded when no profile is selected.
|
|
629
|
+
- A profile file is `handoff.config.<profile>.*`, with the same four
|
|
630
|
+
extensions.
|
|
631
|
+
- Select a profile with `--profile <name>` or with `HANDOFF_PROFILE`.
|
|
632
|
+
`--profile` takes precedence over `HANDOFF_PROFILE`.
|
|
633
|
+
- Profile names are not predefined. A name can contain lowercase letters,
|
|
634
|
+
numbers, and hyphens.
|
|
635
|
+
- A selected profile must exist. If no `handoff.config.<name>.*` file is found,
|
|
636
|
+
the command stops with an error.
|
|
637
|
+
- `login`, `logout`, `publish`, and `checkout` also accept a profile that has
|
|
638
|
+
only a saved registry login, because there the profile selects the registry.
|
|
639
|
+
Every other command still needs the config file.
|
|
640
|
+
- Layers resolve as defaults, base config, profile, then programmatic config,
|
|
641
|
+
with the merge rules described under [Configuration](#configuration).
|
|
642
|
+
- A selected profile also reads `.env.<profile>` on top of `.env`, when that
|
|
643
|
+
file exists. Both are read from the directory the command runs in, and a
|
|
644
|
+
variable already set in the environment beats both files.
|
|
645
|
+
|
|
646
|
+
```bash
|
|
647
|
+
npx handoff-app build --target registry --profile registry
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
`build --target registry` always packages a registry-mode application, so a
|
|
651
|
+
profile does not need to set `runtime.mode`. This makes a profile a good place
|
|
652
|
+
for the registry build settings: the database driver, the database
|
|
653
|
+
environment-variable name, the asset storage, and whether MCP is enabled. A
|
|
654
|
+
hosting provider or a CI job selects the profile through `HANDOFF_PROFILE`.
|
|
655
|
+
|
|
656
|
+
`defineConfig` types a profile as well as a base config. The generated
|
|
657
|
+
`.gitignore` lists `handoff.config.local.*`, which makes `local` the usual name
|
|
658
|
+
for a profile that stays on one machine.
|
|
659
|
+
|
|
660
|
+
Arrays replace, so a profile that declares `catalog.include` also decides what
|
|
661
|
+
`make` and `checkout` register.
|
|
662
|
+
|
|
663
|
+
`.env.<profile>` is read once at startup rather than watched.
|
|
664
|
+
`HANDOFF_WORKING_PATH` cannot be set from it, because the working path is
|
|
665
|
+
resolved before any profile is known.
|
|
666
|
+
|
|
553
667
|
## CLI reference
|
|
554
668
|
|
|
555
669
|
| Command | Description |
|
|
@@ -563,10 +677,12 @@ both.
|
|
|
563
677
|
| `npm run validate` | Configured components are validated |
|
|
564
678
|
| `npm run publish -- <kind\|all> [id...]` | Catalog items, pages, tokens, or assets are published |
|
|
565
679
|
| `npm run checkout -- <kind\|all> [id...]` | Published content is pulled into a workspace |
|
|
566
|
-
| `npm run login -- --url <url>` | The CLI is authorized through the registry device flow |
|
|
567
|
-
| `npm run logout -- [--
|
|
680
|
+
| `npm run login -- [--profile <name>] --url <url>` | The CLI is authorized through the registry device flow |
|
|
681
|
+
| `npm run logout -- [--profile <name>] [--all]` | Saved CLI credentials are revoked and removed |
|
|
568
682
|
|
|
569
|
-
|
|
683
|
+
Every command except `init` accepts `-c, --config`, `--profile`, `-d, --debug`,
|
|
684
|
+
and `-f, --force`. `publish` and `checkout` additionally accept `--dry-run`, and
|
|
685
|
+
`publish` accepts `--no-build`.
|
|
570
686
|
|
|
571
687
|
Arguments after `--` are forwarded to the local CLI. Exact options can be shown
|
|
572
688
|
by adding `--help` after the separator. Advanced configuration and hooks are
|
package/UPGRADE.md
CHANGED
|
@@ -18,10 +18,30 @@ Each feature section identifies the affected projects and the required changes.
|
|
|
18
18
|
|
|
19
19
|
| Change | Affected projects |
|
|
20
20
|
| --- | --- |
|
|
21
|
+
| [Figma token environment references](#figma-token-environment-references) | Projects that put a Figma token directly in config |
|
|
21
22
|
| [Catalog items](#catalog-items) | Projects with component or pattern declarations |
|
|
22
23
|
|
|
23
24
|
<!-- Add future breaking changes as feature sections before "Verify the upgrade". Add each section to this table. -->
|
|
24
25
|
|
|
26
|
+
### Figma token environment references
|
|
27
|
+
|
|
28
|
+
`devAccessToken` and its legacy spelling `dev_access_token` now accept only an environment reference.
|
|
29
|
+
A literal token throws `HandoffConfigError`, including in JavaScript and JSON configs.
|
|
30
|
+
Move the token to the environment and replace the literal:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { defineConfig, fromEnv } from 'handoff-app';
|
|
34
|
+
|
|
35
|
+
export default defineConfig({
|
|
36
|
+
devAccessToken: fromEnv('HANDOFF_DEV_ACCESS_TOKEN'),
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For JSON, use `"devAccessToken": { "$env": "HANDOFF_DEV_ACCESS_TOKEN" }`.
|
|
41
|
+
If you already use `HANDOFF_DEV_ACCESS_TOKEN` and omit the config property, no change is required.
|
|
42
|
+
Environment references resolve after profile merging. Higher-layer literals still override default
|
|
43
|
+
environment seeds for properties that allow literals, such as output directories.
|
|
44
|
+
|
|
25
45
|
### Catalog items
|
|
26
46
|
|
|
27
47
|
#### Affected projects
|
|
@@ -377,13 +377,21 @@ After migrations and deployment, open \`/install\` immediately and create the in
|
|
|
377
377
|
The installer verifies the deployment but never changes the schema. An exposed, uninstalled registry
|
|
378
378
|
can be claimed by its first visitor, so do not leave this step unattended.
|
|
379
379
|
|
|
380
|
-
Once installation completes, sign in and authorize a workspace
|
|
380
|
+
Once installation completes, sign in and authorize a workspace. Either approve the CLI in the
|
|
381
|
+
browser:
|
|
381
382
|
|
|
382
383
|
\`\`\`bash
|
|
383
384
|
handoff-app login --url https://registry.example.com
|
|
384
385
|
\`\`\`
|
|
385
386
|
|
|
386
|
-
|
|
387
|
+
or create a token under Account > Access tokens and set it in the workspace environment:
|
|
388
|
+
|
|
389
|
+
\`\`\`bash
|
|
390
|
+
HANDOFF_REGISTRY_URL=https://registry.example.com
|
|
391
|
+
HANDOFF_REGISTRY_ACCESS_TOKEN=hnd_…
|
|
392
|
+
\`\`\`
|
|
393
|
+
|
|
394
|
+
Both produce the same revocable, user-owned credential. The former
|
|
387
395
|
\`HANDOFF_REGISTRY_API_TOKEN\` fixed server secret is no longer accepted.
|
|
388
396
|
|
|
389
397
|
## Run it (self-hosting)
|
|
@@ -95,7 +95,9 @@ const persistClientConfig = (handoff_1, ...args_1) => __awaiter(void 0, [handoff
|
|
|
95
95
|
// even when packaged from a connected project. Otherwise a workspace is connected whenever a
|
|
96
96
|
// registry URL resolves — from config/env or a saved device login — so `login` alone surfaces
|
|
97
97
|
// the publish/checkout UI without also requiring HANDOFF_REGISTRY_URL.
|
|
98
|
-
const connected = mode === 'registry'
|
|
98
|
+
const connected = mode === 'registry'
|
|
99
|
+
? false
|
|
100
|
+
: Boolean((yield (0, connection_1.resolveAuthenticatedRegistryConnection)(handoff.config, handoff.workingPath, handoff.getProfile())).url);
|
|
99
101
|
clientConfig.runtime = Object.assign(Object.assign({}, clientConfig.runtime), { mode, connected });
|
|
100
102
|
yield fs_extra_1.default.writeJson(path_1.default.resolve(appPath, 'client.config.json'), { config: clientConfig }, { spaces: 2 });
|
|
101
103
|
yield fs_extra_1.default.writeJson(path_1.default.resolve(appPath, 'runtime.server.json'), buildServerRuntimeConfig(handoff.config, options.runtimeModeOverride), { spaces: 2 });
|
|
@@ -264,17 +264,27 @@ const watchRuntimeConfiguration = (handoff, state) => {
|
|
|
264
264
|
const entryBefore = handoff.getConfigFileEntry(changedFile);
|
|
265
265
|
const strategy = entryBefore ? (0, config_diff_1.getStrategy)(entryBefore.kind) : undefined;
|
|
266
266
|
const handle = entryBefore && strategy ? strategy.capture(handoff, entryBefore.entityId) : undefined;
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
let reloadFailed = false;
|
|
268
|
+
try {
|
|
269
|
+
handoff.reload();
|
|
270
|
+
yield (0, client_config_1.persistClientConfig)(handoff);
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
// A failed reload leaves the last config that loaded on the instance, so the
|
|
274
|
+
// re-arm below still names the file that just broke. Without it the file drops
|
|
275
|
+
// out of the watch set - chokidar stops tracking a watched file once it is
|
|
276
|
+
// deleted - and a later valid save is never seen.
|
|
277
|
+
reloadFailed = true;
|
|
278
|
+
logger_1.Logger.error('Error reloading runtime configuration:', e);
|
|
279
|
+
}
|
|
269
280
|
(0, exports.watchRuntimeComponents)(handoff, state, (0, component_1.getRuntimeComponentsPathsToWatch)(handoff));
|
|
270
281
|
(0, exports.watchRuntimeConfiguration)(handoff, state);
|
|
282
|
+
if (reloadFailed) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
271
285
|
const entryAfter = handoff.getConfigFileEntry(changedFile);
|
|
272
|
-
const normalizedChanged = (0, path_2.normalizePathForCompare)(changedFile);
|
|
273
|
-
const normalizedMainConfig = handoff.getMainConfigFilePath()
|
|
274
|
-
? (0, path_2.normalizePathForCompare)(handoff.getMainConfigFilePath())
|
|
275
|
-
: undefined;
|
|
276
286
|
const kindChanged = !!entryBefore && !!entryAfter && entryBefore.kind !== entryAfter.kind;
|
|
277
|
-
if (
|
|
287
|
+
if (handoff.isMainConfigFile(changedFile)) {
|
|
278
288
|
yield (0, builder_1.default)(handoff);
|
|
279
289
|
}
|
|
280
290
|
else if (kindChanged) {
|
|
@@ -35,7 +35,11 @@ const CACHE_VERSION = '1.0.0';
|
|
|
35
35
|
* Gets the path to the build cache file
|
|
36
36
|
*/
|
|
37
37
|
function getCachePath(handoff) {
|
|
38
|
-
|
|
38
|
+
// Two profiles share one project id and can build different output, so the profile has to name
|
|
39
|
+
// the cache file. One shared cache would serve the other profile's components.
|
|
40
|
+
const profile = handoff.getProfile();
|
|
41
|
+
const fileName = profile ? `build-cache.${profile}.json` : 'build-cache.json';
|
|
42
|
+
return path_1.default.resolve(handoff.modulePath, '.handoff', handoff.getProjectId(), '.cache', fileName);
|
|
39
43
|
}
|
|
40
44
|
/**
|
|
41
45
|
* Loads the build cache from disk
|
|
@@ -88,6 +88,9 @@ const normalizeCatalogItem = (moduleExports, options) => {
|
|
|
88
88
|
entries[entryKey] = path_1.default.resolve(directory, source.file);
|
|
89
89
|
// Existing builders consume template as the primary build input for every renderer.
|
|
90
90
|
entries.template = entries[entryKey];
|
|
91
|
+
// The renderer and the hydration bundle both read a preview `render` from the declaration.
|
|
92
|
+
if (options.declarationPath)
|
|
93
|
+
entries.declaration = options.declarationPath;
|
|
91
94
|
// A wrong renderer builds a broken preview without failing on its own, so a mismatch is fatal.
|
|
92
95
|
// `config/runtime.ts` turns the throw into a skipped item that retries on the next save.
|
|
93
96
|
const claim = (0, renderers_1.sourceForFile)(source.file);
|
package/dist/catalog/previews.js
CHANGED
|
@@ -8,16 +8,17 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
|
8
8
|
const lodash_1 = require("lodash");
|
|
9
9
|
/**
|
|
10
10
|
* A declaration module exports more than previews: the item itself, the implementation it
|
|
11
|
-
* re-exports, helper values. Accept only a plain object that owns
|
|
12
|
-
* component or helper is never mistaken for a preview.
|
|
11
|
+
* re-exports, helper values. Accept only a plain object that owns one of the preview fields, so an
|
|
12
|
+
* exported component or helper is never mistaken for a preview.
|
|
13
13
|
*
|
|
14
14
|
* `getCsfStoryEntries` does not fit here: on a CSF file every named export is a story, and on a
|
|
15
15
|
* declaration file it is not.
|
|
16
16
|
*/
|
|
17
|
+
const PREVIEW_KEYS = ['args', 'name', 'render'];
|
|
17
18
|
const isPreviewDeclaration = (value) => {
|
|
18
19
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
19
20
|
return false;
|
|
20
|
-
return
|
|
21
|
+
return PREVIEW_KEYS.some((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
21
22
|
};
|
|
22
23
|
/** Matches an exported binding, an export list, or a CommonJS export assignment. */
|
|
23
24
|
const EXPORT_PATTERNS = [
|
|
@@ -99,7 +100,7 @@ const createCatalogPreviews = (moduleExports, declarationPath) => {
|
|
|
99
100
|
throw new Error(`Preview "${exportName}" uses removed fields. Use args and name. See UPGRADE.md#catalog-items.`);
|
|
100
101
|
}
|
|
101
102
|
const preview = value;
|
|
102
|
-
previews[exportName] = Object.assign({ title: preview.name || (0, lodash_1.startCase)(exportName), values: Object.assign({}, ((_a = preview.args) !== null && _a !== void 0 ? _a : {})), url: (_b = preview.url) !== null && _b !== void 0 ? _b : '' }, (preview.usage ? { usage: preview.usage } : {}));
|
|
103
|
+
previews[exportName] = Object.assign(Object.assign({ title: preview.name || (0, lodash_1.startCase)(exportName), values: Object.assign({}, ((_a = preview.args) !== null && _a !== void 0 ? _a : {})), url: (_b = preview.url) !== null && _b !== void 0 ? _b : '' }, (preview.usage ? { usage: preview.usage } : {})), (typeof preview.render === 'function' ? { render: preview.render } : {}));
|
|
103
104
|
}
|
|
104
105
|
return declarationPath ? (0, exports.orderPreviews)(previews, (0, exports.readExportOrder)(declarationPath)) : previews;
|
|
105
106
|
};
|
package/dist/catalog/types.d.ts
CHANGED
|
@@ -43,6 +43,14 @@ export type CatalogPreview<TArgs = Record<string, unknown>> = {
|
|
|
43
43
|
args?: TArgs;
|
|
44
44
|
usage?: string;
|
|
45
45
|
url?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Renders this preview in place of the implementation, for props it cannot carry as data: an icon
|
|
48
|
+
* component, a callback, JSX children, or state the preview owns.
|
|
49
|
+
*
|
|
50
|
+
* Rendered as a component with `args`, so it can use hooks. The item still documents the
|
|
51
|
+
* implementation, so a wrapper never becomes the published API.
|
|
52
|
+
*/
|
|
53
|
+
render?: (args: TArgs) => unknown;
|
|
46
54
|
};
|
|
47
55
|
/**
|
|
48
56
|
* `__args` exists only in the type system so `Preview<typeof item>` can infer the preview argument type.
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { type CliAuth } from './store';
|
|
2
2
|
export interface LoginOptions {
|
|
3
3
|
openBrowser?: boolean;
|
|
4
|
+
/** Profile the issued credential is saved under. Omitted, it is the default profile. */
|
|
5
|
+
profile?: string;
|
|
4
6
|
}
|
|
5
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Complete the device grant and persist the one-time plaintext access token for this workspace,
|
|
9
|
+
* under the selected profile.
|
|
10
|
+
*/
|
|
6
11
|
export declare const loginWithDevice: (workingPath: string, remoteUrl: string, options?: LoginOptions) => Promise<CliAuth>;
|
|
7
12
|
/**
|
|
8
13
|
* Best-effort remote revocation. The caller must clear the local credential regardless of failure.
|
package/dist/cli/auth/device.js
CHANGED
|
@@ -53,7 +53,10 @@ const shouldOpenBrowser = (options) => {
|
|
|
53
53
|
return process.env.HANDOFF_LOGIN_NO_BROWSER !== '1' && process.env.HANDOFF_LOGIN_NO_BROWSER !== 'true';
|
|
54
54
|
};
|
|
55
55
|
const positiveNumberOr = (value, fallback) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Complete the device grant and persist the one-time plaintext access token for this workspace,
|
|
58
|
+
* under the selected profile.
|
|
59
|
+
*/
|
|
57
60
|
const loginWithDevice = (workingPath_1, remoteUrl_1, ...args_1) => __awaiter(void 0, [workingPath_1, remoteUrl_1, ...args_1], void 0, function* (workingPath, remoteUrl, options = {}) {
|
|
58
61
|
const baseUrl = (0, store_1.normalizeRegistryUrl)(remoteUrl);
|
|
59
62
|
let deviceResponse;
|
|
@@ -115,7 +118,7 @@ const loginWithDevice = (workingPath_1, remoteUrl_1, ...args_1) => __awaiter(voi
|
|
|
115
118
|
accessToken: token.access_token,
|
|
116
119
|
expiresAtMs: Date.now() + expiresIn * 1000,
|
|
117
120
|
};
|
|
118
|
-
yield (0, store_1.writeCliAuth)(workingPath, auth);
|
|
121
|
+
yield (0, store_1.writeCliAuth)(workingPath, auth, options.profile);
|
|
119
122
|
return auth;
|
|
120
123
|
}
|
|
121
124
|
const failure = responseError(token);
|
package/dist/cli/auth/store.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface CliAuth {
|
|
|
5
5
|
/** Epoch milliseconds after which the token must not be used. */
|
|
6
6
|
expiresAtMs: number;
|
|
7
7
|
}
|
|
8
|
+
/** Name a login is saved under when no profile is selected. */
|
|
9
|
+
export declare const DEFAULT_LOGIN_PROFILE = "default";
|
|
8
10
|
/**
|
|
9
11
|
* Canonicalize a registry URL for both HTTP requests and exact credential matching.
|
|
10
12
|
* Base paths are retained; query strings, fragments, credentials, and trailing slashes are not.
|
|
@@ -17,13 +19,20 @@ export declare const normalizeRegistryUrl: (value: string) => string;
|
|
|
17
19
|
*/
|
|
18
20
|
export declare const assertRegistryOriginUrl: (baseUrl: string, candidate: string) => string;
|
|
19
21
|
export declare const cliAuthFilePath: (workingPath: string) => string;
|
|
20
|
-
/**
|
|
21
|
-
export declare const
|
|
22
|
+
/** Every saved login, keyed by profile name. A missing or unreadable file reads as no logins. */
|
|
23
|
+
export declare const readCliLogins: (workingPath: string) => Promise<Record<string, CliAuth>>;
|
|
24
|
+
/** The login saved under one profile name. Never falls back to another profile. */
|
|
25
|
+
export declare const readCliAuth: (workingPath: string, profile?: string) => Promise<CliAuth | null>;
|
|
22
26
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
27
|
+
* Profile names that have a saved login. Synchronous because config loading is, and the loader needs
|
|
28
|
+
* these names to decide whether a selected profile resolves.
|
|
25
29
|
*/
|
|
26
|
-
export declare const
|
|
27
|
-
|
|
30
|
+
export declare const loginProfileNames: (workingPath: string) => string[];
|
|
31
|
+
/** Save the login for one profile and leave the other profiles in place. */
|
|
32
|
+
export declare const writeCliAuth: (workingPath: string, auth: CliAuth, profile?: string) => Promise<void>;
|
|
33
|
+
/** Remove the login for one profile, and the file itself once no login is left. */
|
|
34
|
+
export declare const clearCliAuth: (workingPath: string, profile?: string) => Promise<void>;
|
|
35
|
+
/** Remove every saved login. */
|
|
36
|
+
export declare const clearAllCliAuth: (workingPath: string) => Promise<void>;
|
|
28
37
|
export declare const cliAuthMatchesRegistry: (auth: CliAuth | null, remoteUrl: string) => boolean;
|
|
29
38
|
export declare const cliAuthTokenIsValid: (auth: CliAuth | null, skewMs?: number) => boolean;
|
package/dist/cli/auth/store.js
CHANGED
|
@@ -12,9 +12,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.cliAuthTokenIsValid = exports.cliAuthMatchesRegistry = exports.clearCliAuth = exports.writeCliAuth = exports.readCliAuth = exports.cliAuthFilePath = exports.assertRegistryOriginUrl = exports.normalizeRegistryUrl = void 0;
|
|
15
|
+
exports.cliAuthTokenIsValid = exports.cliAuthMatchesRegistry = exports.clearAllCliAuth = exports.clearCliAuth = exports.writeCliAuth = exports.loginProfileNames = exports.readCliAuth = exports.readCliLogins = exports.cliAuthFilePath = exports.assertRegistryOriginUrl = exports.normalizeRegistryUrl = exports.DEFAULT_LOGIN_PROFILE = void 0;
|
|
16
16
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
17
17
|
const path_1 = __importDefault(require("path"));
|
|
18
|
+
/** Name a login is saved under when no profile is selected. */
|
|
19
|
+
exports.DEFAULT_LOGIN_PROFILE = 'default';
|
|
18
20
|
const AUTH_DIRECTORY = '.handoff';
|
|
19
21
|
const AUTH_FILE = 'cli-auth.json';
|
|
20
22
|
/**
|
|
@@ -63,38 +65,71 @@ const assertRegistryOriginUrl = (baseUrl, candidate) => {
|
|
|
63
65
|
exports.assertRegistryOriginUrl = assertRegistryOriginUrl;
|
|
64
66
|
const cliAuthFilePath = (workingPath) => path_1.default.resolve(workingPath, AUTH_DIRECTORY, AUTH_FILE);
|
|
65
67
|
exports.cliAuthFilePath = cliAuthFilePath;
|
|
66
|
-
|
|
68
|
+
/** An invalid or partial entry reads as no login, so one bad entry cannot hide the others. */
|
|
69
|
+
const toCliAuth = (value) => {
|
|
67
70
|
if (!value || typeof value !== 'object')
|
|
68
|
-
return
|
|
71
|
+
return null;
|
|
69
72
|
const auth = value;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
/** Invalid or partial credential files are treated as unauthenticated. */
|
|
77
|
-
const readCliAuth = (workingPath) => __awaiter(void 0, void 0, void 0, function* () {
|
|
73
|
+
if (typeof auth.remoteUrl !== 'string')
|
|
74
|
+
return null;
|
|
75
|
+
if (typeof auth.accessToken !== 'string' || auth.accessToken.length === 0)
|
|
76
|
+
return null;
|
|
77
|
+
if (typeof auth.expiresAtMs !== 'number' || !Number.isFinite(auth.expiresAtMs))
|
|
78
|
+
return null;
|
|
78
79
|
try {
|
|
79
|
-
|
|
80
|
-
if (!isCliAuth(value))
|
|
81
|
-
return null;
|
|
82
|
-
return Object.assign(Object.assign({}, value), { remoteUrl: (0, exports.normalizeRegistryUrl)(value.remoteUrl) });
|
|
80
|
+
return { remoteUrl: (0, exports.normalizeRegistryUrl)(auth.remoteUrl), accessToken: auth.accessToken, expiresAtMs: auth.expiresAtMs };
|
|
83
81
|
}
|
|
84
82
|
catch (_a) {
|
|
85
83
|
return null;
|
|
86
84
|
}
|
|
85
|
+
};
|
|
86
|
+
const toLogins = (value) => {
|
|
87
|
+
const logins = value === null || value === void 0 ? void 0 : value.logins;
|
|
88
|
+
if (!logins || typeof logins !== 'object')
|
|
89
|
+
return {};
|
|
90
|
+
const result = {};
|
|
91
|
+
for (const [profile, entry] of Object.entries(logins)) {
|
|
92
|
+
const auth = toCliAuth(entry);
|
|
93
|
+
if (auth)
|
|
94
|
+
result[profile] = auth;
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
};
|
|
98
|
+
/** Every saved login, keyed by profile name. A missing or unreadable file reads as no logins. */
|
|
99
|
+
const readCliLogins = (workingPath) => __awaiter(void 0, void 0, void 0, function* () {
|
|
100
|
+
try {
|
|
101
|
+
return toLogins((yield fs_extra_1.default.readJson((0, exports.cliAuthFilePath)(workingPath))));
|
|
102
|
+
}
|
|
103
|
+
catch (_a) {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
87
106
|
});
|
|
107
|
+
exports.readCliLogins = readCliLogins;
|
|
108
|
+
/** The login saved under one profile name. Never falls back to another profile. */
|
|
109
|
+
const readCliAuth = (workingPath_1, ...args_1) => __awaiter(void 0, [workingPath_1, ...args_1], void 0, function* (workingPath, profile = exports.DEFAULT_LOGIN_PROFILE) { var _a; return (_a = (yield (0, exports.readCliLogins)(workingPath))[profile]) !== null && _a !== void 0 ? _a : null; });
|
|
88
110
|
exports.readCliAuth = readCliAuth;
|
|
111
|
+
/**
|
|
112
|
+
* Profile names that have a saved login. Synchronous because config loading is, and the loader needs
|
|
113
|
+
* these names to decide whether a selected profile resolves.
|
|
114
|
+
*/
|
|
115
|
+
const loginProfileNames = (workingPath) => {
|
|
116
|
+
try {
|
|
117
|
+
return Object.keys(toLogins(fs_extra_1.default.readJsonSync((0, exports.cliAuthFilePath)(workingPath))));
|
|
118
|
+
}
|
|
119
|
+
catch (_a) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
exports.loginProfileNames = loginProfileNames;
|
|
89
124
|
/**
|
|
90
125
|
* Atomically replace the credential file. Restrictive permissions are best-effort on platforms
|
|
91
126
|
* whose filesystems do not support POSIX modes.
|
|
92
127
|
*/
|
|
93
|
-
const
|
|
128
|
+
const writeCliLogins = (workingPath, logins) => __awaiter(void 0, void 0, void 0, function* () {
|
|
94
129
|
const filePath = (0, exports.cliAuthFilePath)(workingPath);
|
|
95
130
|
const directory = path_1.default.dirname(filePath);
|
|
96
131
|
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
97
|
-
const value =
|
|
132
|
+
const value = { logins };
|
|
98
133
|
yield fs_extra_1.default.ensureDir(directory, 0o700);
|
|
99
134
|
try {
|
|
100
135
|
yield fs_extra_1.default.chmod(directory, 0o700);
|
|
@@ -123,14 +158,32 @@ const writeCliAuth = (workingPath, auth) => __awaiter(void 0, void 0, void 0, fu
|
|
|
123
158
|
throw error;
|
|
124
159
|
}
|
|
125
160
|
});
|
|
161
|
+
/** Save the login for one profile and leave the other profiles in place. */
|
|
162
|
+
const writeCliAuth = (workingPath_1, auth_1, ...args_1) => __awaiter(void 0, [workingPath_1, auth_1, ...args_1], void 0, function* (workingPath, auth, profile = exports.DEFAULT_LOGIN_PROFILE) {
|
|
163
|
+
const logins = yield (0, exports.readCliLogins)(workingPath);
|
|
164
|
+
logins[profile] = Object.assign(Object.assign({}, auth), { remoteUrl: (0, exports.normalizeRegistryUrl)(auth.remoteUrl) });
|
|
165
|
+
yield writeCliLogins(workingPath, logins);
|
|
166
|
+
});
|
|
126
167
|
exports.writeCliAuth = writeCliAuth;
|
|
127
|
-
|
|
168
|
+
/** Remove the login for one profile, and the file itself once no login is left. */
|
|
169
|
+
const clearCliAuth = (workingPath_1, ...args_1) => __awaiter(void 0, [workingPath_1, ...args_1], void 0, function* (workingPath, profile = exports.DEFAULT_LOGIN_PROFILE) {
|
|
170
|
+
const logins = yield (0, exports.readCliLogins)(workingPath);
|
|
171
|
+
delete logins[profile];
|
|
172
|
+
if (Object.keys(logins).length === 0) {
|
|
173
|
+
yield (0, exports.clearAllCliAuth)(workingPath);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
yield writeCliLogins(workingPath, logins);
|
|
177
|
+
});
|
|
178
|
+
exports.clearCliAuth = clearCliAuth;
|
|
179
|
+
/** Remove every saved login. */
|
|
180
|
+
const clearAllCliAuth = (workingPath) => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
181
|
const filePath = (0, exports.cliAuthFilePath)(workingPath);
|
|
129
182
|
if (yield fs_extra_1.default.pathExists(filePath)) {
|
|
130
183
|
yield fs_extra_1.default.remove(filePath);
|
|
131
184
|
}
|
|
132
185
|
});
|
|
133
|
-
exports.
|
|
186
|
+
exports.clearAllCliAuth = clearAllCliAuth;
|
|
134
187
|
const cliAuthMatchesRegistry = (auth, remoteUrl) => {
|
|
135
188
|
if (!auth)
|
|
136
189
|
return false;
|
|
@@ -4,5 +4,10 @@ export interface LoginArgs extends SharedArgs {
|
|
|
4
4
|
url?: string;
|
|
5
5
|
browser?: boolean;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Saves one registry credential per profile, so a workspace can hold a login for every registry it
|
|
9
|
+
* publishes to. The name is independent of the config profiles, and `login` is the one command that
|
|
10
|
+
* accepts a profile nothing else knows yet.
|
|
11
|
+
*/
|
|
7
12
|
declare const command: CommandModule<{}, LoginArgs>;
|
|
8
13
|
export default command;
|