@forgerock/login-framework-cli 0.0.0-beta-20260527205941 → 0.0.0-beta-20260527211328
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 +40 -28
- package/dist/src/commands/generate.d.ts +1 -1
- package/dist/src/commands/generate.js +11 -8
- package/dist/src/commands/init.d.ts +2 -2
- package/dist/src/commands/init.js +5 -15
- package/dist/src/commands/update.d.ts +2 -2
- package/dist/src/commands/update.js +4 -10
- package/dist/src/config/version.d.ts +0 -3
- package/dist/src/config/version.js +0 -1
- package/dist/src/errors.d.ts +0 -8
- package/dist/src/errors.js +0 -2
- package/dist/src/main.js +2 -2
- package/dist/src/templates/callback/__COMPONENT_SLUG__.mock.ts +22 -0
- package/dist/src/templates/callback/__COMPONENT_SLUG__.stories.js +28 -0
- package/dist/src/templates/callback/__COMPONENT_SLUG__.story.svelte +59 -0
- package/dist/src/templates/callback/__COMPONENT_SLUG__.svelte +10 -36
- package/dist/src/templates/callback/__COMPONENT_SLUG__.utilities.test.ts +2 -2
- package/dist/src/templates/callback/__COMPONENT_SLUG__.utilities.ts +1 -1
- package/dist/src/templates/stage/__COMPONENT_SLUG__.mock.ts +23 -0
- package/dist/src/templates/stage/__COMPONENT_SLUG__.stories.js +43 -0
- package/dist/src/templates/stage/__COMPONENT_SLUG__.story.svelte +47 -0
- package/dist/src/templates/stage/__COMPONENT_SLUG__.svelte +21 -20
- package/dist/src/templates/stage/__COMPONENT_SLUG__.utilities.test.ts +2 -2
- package/dist/src/templates/stage/__COMPONENT_SLUG__.utilities.ts +1 -1
- package/dist/src/templates/tsconfig.json +18 -0
- package/dist/src/utils.d.ts +9 -0
- package/dist/src/utils.js +13 -0
- package/package.json +3 -3
- package/dist/scripts/generate-registry.d.ts +0 -2
- package/dist/scripts/generate-registry.js +0 -20
- package/dist/src/services/registry.d.ts +0 -22
- package/dist/src/services/registry.js +0 -169
- package/dist/templates/callback/__COMPONENT_SLUG__.utilities.d.ts +0 -10
- package/dist/templates/callback/__COMPONENT_SLUG__.utilities.js +0 -12
- package/dist/templates/callback/__COMPONENT_SLUG__.utilities.test.d.ts +0 -1
- package/dist/templates/callback/__COMPONENT_SLUG__.utilities.test.js +0 -7
- package/dist/templates/stage/__COMPONENT_SLUG__.utilities.d.ts +0 -10
- package/dist/templates/stage/__COMPONENT_SLUG__.utilities.js +0 -12
- package/dist/templates/stage/__COMPONENT_SLUG__.utilities.test.d.ts +0 -1
- package/dist/templates/stage/__COMPONENT_SLUG__.utilities.test.js +0 -7
package/README.md
CHANGED
|
@@ -35,10 +35,10 @@ pnpm dev
|
|
|
35
35
|
|
|
36
36
|
**Options**
|
|
37
37
|
|
|
38
|
-
| Flag
|
|
39
|
-
|
|
|
40
|
-
| `--
|
|
41
|
-
| `--local <path>`
|
|
38
|
+
| Flag | Description |
|
|
39
|
+
| ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| `--tag <tag>` | Download a specific release tag (e.g. `v1.2.0`). Defaults to latest. |
|
|
41
|
+
| `--local <path>` | Use a local framework directory instead of downloading from GitHub. Useful for development or air-gapped environments. |
|
|
42
42
|
|
|
43
43
|
**What it does**
|
|
44
44
|
|
|
@@ -47,9 +47,9 @@ pnpm dev
|
|
|
47
47
|
3. Writes a minimal `pnpm-workspace.yaml`
|
|
48
48
|
4. Creates `experimental/custom/callbacks/` and `experimental/custom/stages/` with `.gitkeep` placeholders
|
|
49
49
|
5. Copies the component authoring guide to `experimental/custom/README.md`
|
|
50
|
-
6.
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
6. Writes `.generator-version` to track the framework version in use
|
|
51
|
+
|
|
52
|
+
The `core/journey/_utilities/registry/custom-registry.ts` file is generated by the framework's Vite plugin on the first `pnpm dev` or `pnpm build:widget` — the CLI never touches it.
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
@@ -57,7 +57,7 @@ pnpm dev
|
|
|
57
57
|
|
|
58
58
|
### `ping-lf generate stage <Name>`
|
|
59
59
|
|
|
60
|
-
Scaffolds a new custom component under `experimental/custom/callbacks/<slug>/` or `experimental/custom/stages/<slug
|
|
60
|
+
Scaffolds a new custom component under `experimental/custom/callbacks/<slug>/` or `experimental/custom/stages/<slug>/`. The Vite plugin watches these directories during `pnpm dev` and regenerates `custom-registry.ts` automatically on add/remove/change.
|
|
61
61
|
|
|
62
62
|
Run from the root of an initialized project.
|
|
63
63
|
|
|
@@ -81,7 +81,7 @@ experimental/custom/callbacks/name-callback/
|
|
|
81
81
|
└── name-callback.utilities.test.ts # unit test stub
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
The `@component` header in the generated `.svelte` file declares the type and name. The
|
|
84
|
+
The `@component` header in the generated `.svelte` file declares the type and name. The framework's Vite plugin reads this header to register the component:
|
|
85
85
|
|
|
86
86
|
```svelte
|
|
87
87
|
<!--
|
|
@@ -99,13 +99,13 @@ See `experimental/custom/README.md` for the full prop contract.
|
|
|
99
99
|
|
|
100
100
|
### `ping-lf update`
|
|
101
101
|
|
|
102
|
-
Fetches the latest (or a specified) framework version and overwrites the core framework files while **preserving** `experimental/custom/` and project-level configs.
|
|
102
|
+
Fetches the latest (or a specified) framework version and overwrites the core framework files while **preserving** `experimental/custom/` and project-level configs. The component registry is regenerated by the Vite plugin on the next build or dev-server start.
|
|
103
103
|
|
|
104
104
|
Run from the root of an initialized project.
|
|
105
105
|
|
|
106
106
|
```sh
|
|
107
107
|
ping-lf update # update to latest
|
|
108
|
-
ping-lf update --
|
|
108
|
+
ping-lf update --tag v1.5.0 # pin to a specific release tag
|
|
109
109
|
ping-lf update --local ../framework # use a local directory
|
|
110
110
|
```
|
|
111
111
|
|
|
@@ -139,32 +139,45 @@ my-login-project/
|
|
|
139
139
|
|
|
140
140
|
## Development
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
The CLI is a workspace package inside the framework monorepo, but it is **not** declared as a dependency of any other workspace. This is intentional: customer-generated projects must not carry a `workspace:*` link to this CLI, because that protocol only resolves inside this monorepo. The trade-off is that after a fresh clone, `ping-lf` is not on your `$PATH` until you set it up.
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
cd tools/cli
|
|
144
|
+
### Recommended setup (once per machine)
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
pnpm install
|
|
146
|
+
From the repo root, after `pnpm install`:
|
|
149
147
|
|
|
150
|
-
|
|
151
|
-
pnpm build
|
|
148
|
+
```sh
|
|
149
|
+
pnpm --filter @forgerock/login-framework-cli build
|
|
150
|
+
pnpm --filter @forgerock/login-framework-cli link --global
|
|
151
|
+
```
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
pnpm test
|
|
153
|
+
`pnpm link --global` symlinks the CLI's `bin` (`tools/cli/dist/src/main.js`) into your machine's global pnpm bin directory, pointing back at this repo's `dist/`. After this, `ping-lf` works as a real command from anywhere:
|
|
155
154
|
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
```sh
|
|
156
|
+
ping-lf init my-project
|
|
157
|
+
ping-lf generate callback MyCallback
|
|
158
|
+
ping-lf update --tag v1.5.0
|
|
158
159
|
```
|
|
159
160
|
|
|
160
|
-
|
|
161
|
+
Rebuilds (`pnpm --filter @forgerock/login-framework-cli build`) update the global binary automatically — no relink needed.
|
|
162
|
+
|
|
163
|
+
To remove the global link later: `pnpm --filter @forgerock/login-framework-cli unlink --global`.
|
|
164
|
+
|
|
165
|
+
### Without the global link
|
|
166
|
+
|
|
167
|
+
If you'd rather not link globally, invoke the built binary directly. You still need to build first:
|
|
161
168
|
|
|
162
169
|
```sh
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
pnpm --filter @forgerock/login-framework-cli build
|
|
171
|
+
./tools/cli/dist/src/main.js init my-project
|
|
172
|
+
# or:
|
|
173
|
+
pnpm --filter @forgerock/login-framework-cli exec ping-lf init my-project
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Other useful commands
|
|
165
177
|
|
|
166
|
-
|
|
167
|
-
|
|
178
|
+
```sh
|
|
179
|
+
pnpm --filter @forgerock/login-framework-cli test # run unit tests
|
|
180
|
+
pnpm --filter @forgerock/login-framework-cli run dev # tsc --watch
|
|
168
181
|
```
|
|
169
182
|
|
|
170
183
|
### Project structure
|
|
@@ -180,7 +193,6 @@ tools/cli/
|
|
|
180
193
|
│ │ └── update.ts # ping-lf update
|
|
181
194
|
│ ├── services/
|
|
182
195
|
│ │ ├── file-system.ts # copyWithExclusions
|
|
183
|
-
│ │ ├── registry.ts # component scanning + registry generation
|
|
184
196
|
│ │ ├── release.ts # GitHub release fetch
|
|
185
197
|
│ │ └── vite-config.ts # Vite plugin injection
|
|
186
198
|
│ └── config/
|
|
@@ -19,7 +19,7 @@ export declare const StageNameSchema: Schema.transform<Schema.filter<typeof Sche
|
|
|
19
19
|
name: typeof Schema.String;
|
|
20
20
|
slug: typeof Schema.String;
|
|
21
21
|
}>>;
|
|
22
|
-
export declare const generateCommand: Command.Command<"generate", FileSystem.FileSystem | Path.Path | import("@effect/platform/Terminal").Terminal, import("../errors.js").GeneratorVersionError |
|
|
22
|
+
export declare const generateCommand: Command.Command<"generate", FileSystem.FileSystem | Path.Path | import("@effect/platform/Terminal").Terminal, import("../errors.js").GeneratorVersionError | InvalidComponentNameError | ComponentAlreadyExistsError | import("@effect/platform/Error").PlatformError | import("@effect/platform/Terminal").QuitException, {
|
|
23
23
|
readonly subcommand: import("effect/Option").Option<{
|
|
24
24
|
readonly name: string;
|
|
25
25
|
} | {
|
|
@@ -5,7 +5,7 @@ import nodePath from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { assertValidProject } from '../config/version.js';
|
|
7
7
|
import { ComponentAlreadyExistsError, InvalidComponentNameError } from '../errors.js';
|
|
8
|
-
import {
|
|
8
|
+
import { toPascalCase } from '../utils.js';
|
|
9
9
|
const CUSTOM_DIR = 'experimental/custom';
|
|
10
10
|
/**
|
|
11
11
|
* Converts a PascalCase name to a kebab-case slug.
|
|
@@ -85,28 +85,31 @@ function scaffoldComponent(type, name) {
|
|
|
85
85
|
// ── Create component directory ─────────────────────────────────────────
|
|
86
86
|
yield* fs.makeDirectory(componentDir, { recursive: true });
|
|
87
87
|
// ── Copy and process template files ───────────────────────────────────
|
|
88
|
-
// Each file name contains __COMPONENT_SLUG__; its contents
|
|
89
|
-
//
|
|
88
|
+
// Each file name contains __COMPONENT_SLUG__; its contents use:
|
|
89
|
+
// __COMPONENT_NAME__ — raw display name (may contain spaces, e.g. "My Login Stage")
|
|
90
|
+
// __COMPONENT_NAME_PASCAL__ — PascalCase identifier derived from slug (e.g. "MyLoginStage"),
|
|
91
|
+
// safe for use in TypeScript symbol positions (function names etc.)
|
|
92
|
+
// __COMPONENT_SLUG__ — kebab-case slug (e.g. "my-login-stage")
|
|
93
|
+
const pascalName = toPascalCase(name);
|
|
90
94
|
const templateFiles = yield* fs.readDirectory(templatesDir);
|
|
91
95
|
const createdFiles = [];
|
|
92
96
|
for (const templateFile of templateFiles) {
|
|
93
97
|
const targetFileName = templateFile.replaceAll('__COMPONENT_SLUG__', slug);
|
|
94
98
|
const sourceContent = yield* fs.readFileString(p.join(templatesDir, templateFile));
|
|
95
99
|
const processedContent = sourceContent
|
|
100
|
+
.replaceAll('__COMPONENT_NAME_PASCAL__', pascalName)
|
|
96
101
|
.replaceAll('__COMPONENT_NAME__', name)
|
|
97
102
|
.replaceAll('__COMPONENT_SLUG__', slug);
|
|
98
103
|
const targetPath = p.join(componentDir, targetFileName);
|
|
99
104
|
yield* fs.writeFileString(targetPath, processedContent);
|
|
100
105
|
createdFiles.push(targetPath);
|
|
101
106
|
}
|
|
102
|
-
// ── Regenerate custom-registry.ts ────────────────────────────────────
|
|
103
|
-
yield* Console.log('Regenerating custom component registry...');
|
|
104
|
-
yield* runRegistryScript(cwd);
|
|
105
107
|
// ── Print summary ──────────────────────────────────────────────────────
|
|
106
108
|
yield* Console.log(`Done. ${type} component scaffolded successfully.\n\n` +
|
|
107
109
|
`Files created:\n` +
|
|
108
|
-
createdFiles.map((
|
|
109
|
-
`\n\nNext: open ${p.join(componentDir, `${slug}.svelte`)} and implement your component.\n`
|
|
110
|
+
createdFiles.map((createdPath) => ` ${createdPath}`).join('\n') +
|
|
111
|
+
`\n\nNext: open ${p.join(componentDir, `${slug}.svelte`)} and implement your component.\n` +
|
|
112
|
+
`If a dev server is running, the registry will pick up the new component automatically.\n`);
|
|
110
113
|
});
|
|
111
114
|
}
|
|
112
115
|
// ── Subcommands ──────────────────────────────────────────────────────────────
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Command } from '@effect/cli';
|
|
2
2
|
import { FileSystem, Path } from '@effect/platform';
|
|
3
3
|
import { DirectoryConflictError, DirectoryNotEmptyError } from '../errors.js';
|
|
4
|
-
export declare const initCommand: Command.Command<"init", FileSystem.FileSystem | Path.Path | import("../services/release.js").Release, import("../errors.js").InvalidVersionError | import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseFsError | import("../errors.js").FileSystemError |
|
|
4
|
+
export declare const initCommand: Command.Command<"init", FileSystem.FileSystem | Path.Path | import("../services/release.js").Release, import("../errors.js").InvalidVersionError | import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseFsError | import("../errors.js").FileSystemError | DirectoryConflictError | DirectoryNotEmptyError | import("@effect/platform/Error").PlatformError, {
|
|
5
5
|
readonly directory: string;
|
|
6
6
|
readonly local: import("effect/Option").Option<string>;
|
|
7
|
-
readonly
|
|
7
|
+
readonly tag: import("effect/Option").Option<string>;
|
|
8
8
|
}>;
|
|
@@ -4,12 +4,9 @@ import { Console, Effect } from 'effect';
|
|
|
4
4
|
import { writeVersion } from '../config/version.js';
|
|
5
5
|
import { DirectoryConflictError, DirectoryNotEmptyError } from '../errors.js';
|
|
6
6
|
import { copyWithExclusions, expandTilde, isFrameworkDirectory } from '../services/file-system.js';
|
|
7
|
-
import { runRegistryScript } from '../services/registry.js';
|
|
8
7
|
import { resolveSource } from './source.js';
|
|
9
8
|
/**
|
|
10
9
|
* Minimal pnpm-workspace.yaml written to the customer project.
|
|
11
|
-
* Intentionally omits `tools/` — that workspace entry exists only in the
|
|
12
|
-
* upstream monorepo and is not needed (or valid) in a customer project.
|
|
13
10
|
*/
|
|
14
11
|
const PNPM_WORKSPACE = `packages:
|
|
15
12
|
- 'packages/*'
|
|
@@ -34,8 +31,8 @@ Read the authoring guide: experimental/custom/README.md
|
|
|
34
31
|
export const initCommand = Command.make('init', {
|
|
35
32
|
directory: Args.text({ name: 'directory' }).pipe(Args.withDescription('Directory to initialize the project in. Use "./" for the current directory.')),
|
|
36
33
|
local: Options.optional(Options.text('local').pipe(Options.withDescription('Path to a local framework directory. If omitted, the main branch is downloaded from GitHub.'))),
|
|
37
|
-
|
|
38
|
-
}, ({ directory, local,
|
|
34
|
+
tag: Options.optional(Options.text('tag').pipe(Options.withDescription('Framework release tag to download (e.g. v1.2.0). If omitted, the main branch is used.'))),
|
|
35
|
+
}, ({ directory, local, tag }) => Effect.gen(function* () {
|
|
39
36
|
const fs = yield* FileSystem.FileSystem;
|
|
40
37
|
const path = yield* Path.Path;
|
|
41
38
|
const resolvedDir = path.resolve(expandTilde(directory));
|
|
@@ -58,7 +55,7 @@ export const initCommand = Command.make('init', {
|
|
|
58
55
|
// Effect.scoped closes the Scope that release.fetch opens, triggering
|
|
59
56
|
// automatic removal of the .framework-tmp directory after copying.
|
|
60
57
|
const resolvedVersion = yield* Effect.scoped(Effect.gen(function* () {
|
|
61
|
-
const { sourceDir, resolvedVersion } = yield* resolveSource(local,
|
|
58
|
+
const { sourceDir, resolvedVersion } = yield* resolveSource(local, tag, resolvedDir);
|
|
62
59
|
// ── 2. Create target directory + copy framework (with exclusions) ──
|
|
63
60
|
yield* Console.log('Copying framework files...');
|
|
64
61
|
yield* fs.makeDirectory(resolvedDir, { recursive: true });
|
|
@@ -78,18 +75,11 @@ export const initCommand = Command.make('init', {
|
|
|
78
75
|
yield* fs.makeDirectory(customDir, { recursive: true });
|
|
79
76
|
yield* fs.writeFileString(path.join(customDir, '.gitkeep'), '');
|
|
80
77
|
}), { concurrency: 'unbounded', discard: true });
|
|
81
|
-
// ── 5.
|
|
82
|
-
// Run the canonical registry script so the file is immediately present
|
|
83
|
-
// with the correct CustomRegistryEntry format. The empty callbacks/ and
|
|
84
|
-
// stages/ dirs produce empty registries — no custom components yet.
|
|
85
|
-
yield* Console.log('Generating custom component registry...');
|
|
86
|
-
yield* runRegistryScript(resolvedDir);
|
|
87
|
-
// ── 6. Write .generator-version ────────────────────────────────────────
|
|
78
|
+
// ── 5. Write .generator-version ────────────────────────────────────────
|
|
88
79
|
yield* writeVersion(resolvedDir, {
|
|
89
80
|
version: resolvedVersion,
|
|
90
|
-
commitHash: '',
|
|
91
81
|
generatedAt: new Date().toISOString(),
|
|
92
82
|
});
|
|
93
|
-
// ──
|
|
83
|
+
// ── 6. Print next steps ───────────────────────────────────────────────
|
|
94
84
|
yield* Console.log(nextStepsMessage(directory));
|
|
95
85
|
})).pipe(Command.withDescription('Bootstrap a new Ping Login Widget or Login App project from a GitHub release or a local framework path.'));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from '@effect/cli';
|
|
2
|
-
export declare const updateCommand: Command.Command<"update", import("@effect/platform/FileSystem").FileSystem | import("@effect/platform/Path").Path | import("../services/release.js").Release, import("../errors.js").InvalidVersionError | import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseFsError | import("../errors.js").FileSystemError | import("../errors.js").GeneratorVersionError | import("
|
|
2
|
+
export declare const updateCommand: Command.Command<"update", import("@effect/platform/FileSystem").FileSystem | import("@effect/platform/Path").Path | import("../services/release.js").Release, import("../errors.js").InvalidVersionError | import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseFsError | import("../errors.js").FileSystemError | import("../errors.js").GeneratorVersionError | import("@effect/platform/Error").PlatformError, {
|
|
3
3
|
readonly local: import("effect/Option").Option<string>;
|
|
4
|
-
readonly
|
|
4
|
+
readonly tag: import("effect/Option").Option<string>;
|
|
5
5
|
}>;
|
|
@@ -2,12 +2,11 @@ import { Command, Options } from '@effect/cli';
|
|
|
2
2
|
import { Console, Effect } from 'effect';
|
|
3
3
|
import { assertValidProject, writeVersion } from '../config/version.js';
|
|
4
4
|
import { copyWithExclusions } from '../services/file-system.js';
|
|
5
|
-
import { runRegistryScript } from '../services/registry.js';
|
|
6
5
|
import { resolveSource } from './source.js';
|
|
7
6
|
export const updateCommand = Command.make('update', {
|
|
8
7
|
local: Options.optional(Options.text('local').pipe(Options.withDescription('Path to a local framework directory. If omitted, the main branch is downloaded from GitHub.'))),
|
|
9
|
-
|
|
10
|
-
}, ({ local,
|
|
8
|
+
tag: Options.optional(Options.text('tag').pipe(Options.withDescription('Framework release tag to update to (e.g. v1.2.0). If omitted, the main branch is used.'))),
|
|
9
|
+
}, ({ local, tag }) => Effect.gen(function* () {
|
|
11
10
|
const cwd = process.cwd();
|
|
12
11
|
// ── 1. Verify this is an initialized project ──────────────────────────
|
|
13
12
|
const currentVersion = yield* assertValidProject(cwd);
|
|
@@ -16,21 +15,16 @@ export const updateCommand = Command.make('update', {
|
|
|
16
15
|
// Effect.scoped closes the Scope that release.fetch opens, triggering
|
|
17
16
|
// automatic removal of the .framework-tmp directory after copying.
|
|
18
17
|
const resolvedVersion = yield* Effect.scoped(Effect.gen(function* () {
|
|
19
|
-
const { sourceDir, resolvedVersion } = yield* resolveSource(local,
|
|
18
|
+
const { sourceDir, resolvedVersion } = yield* resolveSource(local, tag, cwd);
|
|
20
19
|
yield* Console.log('Copying updated framework files...');
|
|
21
20
|
yield* copyWithExclusions(sourceDir, cwd);
|
|
22
21
|
return resolvedVersion;
|
|
23
22
|
}));
|
|
24
|
-
// ── 3.
|
|
25
|
-
yield* Console.log('Regenerating custom component registry...');
|
|
26
|
-
yield* runRegistryScript(cwd);
|
|
27
|
-
// ── 4. Update .generator-version ──────────────────────────────────────
|
|
23
|
+
// ── 3. Update .generator-version ──────────────────────────────────────
|
|
28
24
|
yield* writeVersion(cwd, {
|
|
29
25
|
version: resolvedVersion,
|
|
30
|
-
commitHash: '',
|
|
31
26
|
generatedAt: new Date().toISOString(),
|
|
32
27
|
});
|
|
33
28
|
yield* Console.log(`\nDone. Updated from ${currentVersion.version} to ${resolvedVersion}.\n` +
|
|
34
|
-
'Custom component registry regenerated.\n' +
|
|
35
29
|
'Run "pnpm install" if package dependencies changed.\n');
|
|
36
30
|
})).pipe(Command.withDescription('Fetch the latest (or a specified) framework version and overwrite core files while preserving experimental/custom/.'));
|
|
@@ -3,13 +3,11 @@ import { Effect, Schema } from 'effect';
|
|
|
3
3
|
import { GeneratorVersionError } from '../errors.js';
|
|
4
4
|
declare const GeneratorVersionSchema: Schema.Struct<{
|
|
5
5
|
version: typeof Schema.String;
|
|
6
|
-
commitHash: typeof Schema.String;
|
|
7
6
|
generatedAt: typeof Schema.String;
|
|
8
7
|
}>;
|
|
9
8
|
export type GeneratorVersion = typeof GeneratorVersionSchema.Type;
|
|
10
9
|
export declare const readVersion: (directory: string) => Effect.Effect<{
|
|
11
10
|
readonly version: string;
|
|
12
|
-
readonly commitHash: string;
|
|
13
11
|
readonly generatedAt: string;
|
|
14
12
|
}, GeneratorVersionError | import("@effect/platform/Error").PlatformError, FileSystem.FileSystem>;
|
|
15
13
|
export declare const writeVersion: (directory: string, version: GeneratorVersion) => Effect.Effect<void, import("@effect/platform/Error").PlatformError, FileSystem.FileSystem>;
|
|
@@ -20,7 +18,6 @@ export declare const writeVersion: (directory: string, version: GeneratorVersion
|
|
|
20
18
|
*/
|
|
21
19
|
export declare const assertValidProject: (directory: string) => Effect.Effect<{
|
|
22
20
|
readonly version: string;
|
|
23
|
-
readonly commitHash: string;
|
|
24
21
|
readonly generatedAt: string;
|
|
25
22
|
}, GeneratorVersionError | import("@effect/platform/Error").PlatformError, FileSystem.FileSystem>;
|
|
26
23
|
export {};
|
|
@@ -3,7 +3,6 @@ import { Console, Effect, Schema } from 'effect';
|
|
|
3
3
|
import { GeneratorVersionError } from '../errors.js';
|
|
4
4
|
const GeneratorVersionSchema = Schema.Struct({
|
|
5
5
|
version: Schema.String,
|
|
6
|
-
commitHash: Schema.String,
|
|
7
6
|
generatedAt: Schema.String,
|
|
8
7
|
});
|
|
9
8
|
const VERSION_FILE = '.generator-version';
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -51,14 +51,6 @@ export declare class GeneratorVersionError extends GeneratorVersionError_base<{
|
|
|
51
51
|
readonly path?: string;
|
|
52
52
|
}> {
|
|
53
53
|
}
|
|
54
|
-
declare const RegistryScanError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
55
|
-
readonly _tag: "RegistryScanError";
|
|
56
|
-
} & Readonly<A>;
|
|
57
|
-
export declare class RegistryScanError extends RegistryScanError_base<{
|
|
58
|
-
readonly directory: string;
|
|
59
|
-
readonly cause?: unknown;
|
|
60
|
-
}> {
|
|
61
|
-
}
|
|
62
54
|
declare const DirectoryConflictError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
63
55
|
readonly _tag: "DirectoryConflictError";
|
|
64
56
|
} & Readonly<A>;
|
package/dist/src/errors.js
CHANGED
|
@@ -13,8 +13,6 @@ export class FileSystemError extends Data.TaggedError('FileSystemError') {
|
|
|
13
13
|
}
|
|
14
14
|
export class GeneratorVersionError extends Data.TaggedError('GeneratorVersionError') {
|
|
15
15
|
}
|
|
16
|
-
export class RegistryScanError extends Data.TaggedError('RegistryScanError') {
|
|
17
|
-
}
|
|
18
16
|
export class DirectoryConflictError extends Data.TaggedError('DirectoryConflictError') {
|
|
19
17
|
}
|
|
20
18
|
export class DirectoryNotEmptyError extends Data.TaggedError('DirectoryNotEmptyError') {
|
package/dist/src/main.js
CHANGED
|
@@ -10,7 +10,7 @@ import { updateCommand } from './commands/update.js';
|
|
|
10
10
|
import { GithubReleaseLayer } from './services/release.js';
|
|
11
11
|
// Read the version from package.json at runtime so it stays in sync
|
|
12
12
|
// with the published package version after changesets bumps it.
|
|
13
|
-
const { version } = createRequire(import.meta.url)('
|
|
13
|
+
const { version } = createRequire(import.meta.url)('../../package.json');
|
|
14
14
|
const rootCommand = Command.make('ping-lf').pipe(Command.withDescription('CLI for initializing, scaffolding, and updating Ping Login Widget and Login App custom component projects.'), Command.withSubcommands([initCommand, generateCommand, updateCommand, releasesCommand]));
|
|
15
15
|
const cli = Command.run(rootCommand, {
|
|
16
16
|
name: 'ping-lf',
|
|
@@ -23,7 +23,7 @@ cli(process.argv).pipe(Effect.catchTag('DirectoryConflictError', (err) => Consol
|
|
|
23
23
|
` ${err.cause}\n\n` +
|
|
24
24
|
` • Check your network connection and try again.\n` +
|
|
25
25
|
` • Use a local path: ping-lf init <dir> --local <path>\n` +
|
|
26
|
-
` • Specify a
|
|
26
|
+
` • Specify a tag: ping-lf init <dir> --tag v1.0.0\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('ReleaseParseError', (err) => Console.error(`\nError: Failed to parse the release data.\n ${err.cause}\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('ReleaseFsError', (err) => Console.error(`\nError: Filesystem error during release download (${err.operation}).\n ${err.cause}\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('InvalidVersionError', (err) => Console.error(`\nError: "${err.version}" is not a valid version tag.\n` +
|
|
27
27
|
` Expected semver format like v1.0.0.\n` +
|
|
28
28
|
` Use "ping-lf releases" to list available versions.\n`).pipe(Effect.andThen(Effect.die(err)))), Effect.catchTag('ReleaseNotFoundError', (err) => Console.error(`\nError: No releases found on GitHub.` +
|
|
29
29
|
(err.cause ? `\n ${err.cause}` : '') +
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock journey step for the __COMPONENT_NAME__ callback's Storybook stories.
|
|
3
|
+
*
|
|
4
|
+
* Replace the placeholder `type`, `output`, and `input` fields with the real
|
|
5
|
+
* AM payload your callback consumes. The shape mirrors what AM sends back from
|
|
6
|
+
* an authentication tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createJourneyStep } from '$login-framework';
|
|
10
|
+
|
|
11
|
+
export default createJourneyStep({
|
|
12
|
+
authId: 'test-auth-id',
|
|
13
|
+
callbacks: [
|
|
14
|
+
{
|
|
15
|
+
type: 'NameCallback',
|
|
16
|
+
output: [{ name: 'prompt', value: '__COMPONENT_NAME__ prompt' }],
|
|
17
|
+
input: [{ name: 'IDToken1', value: '' }],
|
|
18
|
+
_id: 0,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
stage: 'DefaultLogin',
|
|
22
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storybook stories for the __COMPONENT_NAME__ callback.
|
|
3
|
+
*
|
|
4
|
+
* Update the `getCallback` helper if your mock contains more than one callback,
|
|
5
|
+
* or add additional stories for variants (loading, error, prefilled, etc.).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import step from './__COMPONENT_SLUG__.mock';
|
|
9
|
+
import Story from './__COMPONENT_SLUG__.story.svelte';
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
argTypes: {
|
|
13
|
+
callback: { control: false },
|
|
14
|
+
},
|
|
15
|
+
component: Story,
|
|
16
|
+
parameters: {
|
|
17
|
+
layout: 'fullscreen',
|
|
18
|
+
},
|
|
19
|
+
title: 'Custom/Callback/__COMPONENT_NAME__',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const getCallback = () => step.callbacks[0];
|
|
23
|
+
|
|
24
|
+
export const Base = {
|
|
25
|
+
args: {
|
|
26
|
+
callback: getCallback(),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Story wrapper for the __COMPONENT_NAME__ callback. Mounts the component
|
|
3
|
+
with default metadata so it can render in isolation inside Storybook.
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import {
|
|
8
|
+
type BaseCallback,
|
|
9
|
+
type CallbackMetadata,
|
|
10
|
+
Centered,
|
|
11
|
+
type Maybe,
|
|
12
|
+
type SelfSubmitFunction,
|
|
13
|
+
type StepMetadata,
|
|
14
|
+
type StyleObject,
|
|
15
|
+
} from '$login-framework';
|
|
16
|
+
import __COMPONENT_NAME_PASCAL__ from './__COMPONENT_SLUG__.svelte';
|
|
17
|
+
|
|
18
|
+
export let callback: BaseCallback;
|
|
19
|
+
export let callbackMetadata: Maybe<CallbackMetadata> = undefined;
|
|
20
|
+
export let style: StyleObject = {};
|
|
21
|
+
export let selfSubmitFunction: Maybe<SelfSubmitFunction> = null;
|
|
22
|
+
export let stepMetadata: Maybe<StepMetadata> = null;
|
|
23
|
+
|
|
24
|
+
const defaultCallbackMetadata: CallbackMetadata = {
|
|
25
|
+
derived: {
|
|
26
|
+
canForceUserInputOptionality: false,
|
|
27
|
+
isFirstInvalidInput: false,
|
|
28
|
+
isReadyForSubmission: false,
|
|
29
|
+
isSelfSubmitting: false,
|
|
30
|
+
isUserInputRequired: true,
|
|
31
|
+
isPasskeyAutofillEligible: false,
|
|
32
|
+
},
|
|
33
|
+
idx: 0,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const defaultStepMetadata: StepMetadata = {
|
|
37
|
+
derived: {
|
|
38
|
+
isStepSelfSubmittable: () => false,
|
|
39
|
+
isUserInputOptional: false,
|
|
40
|
+
numOfCallbacks: 1,
|
|
41
|
+
numOfSelfSubmittableCbs: 0,
|
|
42
|
+
numOfUserInputCbs: 1,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
$: mergedCallbackMetadata = { ...defaultCallbackMetadata, ...callbackMetadata };
|
|
47
|
+
$: mergedStepMetadata = stepMetadata ?? defaultStepMetadata;
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<Centered>
|
|
51
|
+
<svelte:component
|
|
52
|
+
this={__COMPONENT_NAME_PASCAL__}
|
|
53
|
+
{callback}
|
|
54
|
+
callbackMetadata={mergedCallbackMetadata}
|
|
55
|
+
{style}
|
|
56
|
+
{selfSubmitFunction}
|
|
57
|
+
stepMetadata={mergedStepMetadata}
|
|
58
|
+
/>
|
|
59
|
+
</Centered>
|
|
@@ -7,46 +7,20 @@ Custom callback component. Replace this description with your own.
|
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
<script lang="ts">
|
|
10
|
-
import type {
|
|
11
|
-
import type { z } from 'zod';
|
|
12
|
-
|
|
13
|
-
import type { Maybe } from '$core/interfaces';
|
|
14
|
-
import type { styleSchema } from '$core/style.store';
|
|
15
|
-
import type {
|
|
16
|
-
CallbackMetadata,
|
|
17
|
-
SelfSubmitFunction,
|
|
18
|
-
StepMetadata,
|
|
19
|
-
} from '$journey/journey.interfaces';
|
|
10
|
+
import type { BaseCallback } from '@forgerock/journey-client/types';
|
|
20
11
|
|
|
21
12
|
/**
|
|
22
|
-
* The
|
|
13
|
+
* The callback instance for this component. Use `callback.getInputValue()`
|
|
23
14
|
* and `callback.setInputValue()` to read/write values sent back to the server.
|
|
24
15
|
*/
|
|
25
|
-
export let callback:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Metadata about the current step (page node) — includes stage name,
|
|
35
|
-
* header/description overrides, and derived helpers like `isStepSelfSubmittable()`.
|
|
36
|
-
*/
|
|
37
|
-
export const stepMetadata: Maybe<StepMetadata> = null;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Per-callback metadata from the step — may include policies, failed policies,
|
|
41
|
-
* and other AM-provided hints for rendering.
|
|
42
|
-
*/
|
|
43
|
-
export const callbackMetadata: Maybe<CallbackMetadata> = null;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* The widget's resolved style configuration (colors, logos, labels, etc.).
|
|
47
|
-
* Use this to keep your custom callback visually consistent with the theme.
|
|
48
|
-
*/
|
|
49
|
-
export const style: z.infer<typeof styleSchema> = {};
|
|
16
|
+
export let callback: BaseCallback;
|
|
17
|
+
|
|
18
|
+
// Optionally declare any of these if your component needs them:
|
|
19
|
+
// import type { CallbackMetadata, Maybe, SelfSubmitFunction, StepMetadata, StyleObject } from '$login-framework';
|
|
20
|
+
// export let selfSubmitFunction: Maybe<SelfSubmitFunction> = null;
|
|
21
|
+
// export let stepMetadata: Maybe<StepMetadata> = null;
|
|
22
|
+
// export let callbackMetadata: Maybe<CallbackMetadata> = null;
|
|
23
|
+
// export let style: StyleObject = {};
|
|
50
24
|
|
|
51
25
|
// Suppress the "unused export" warning — remove `void` once you use `callback`.
|
|
52
26
|
void callback;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { format__COMPONENT_NAME_PASCAL__Label } from './__COMPONENT_SLUG__.utilities.js';
|
|
4
4
|
|
|
5
5
|
describe('__COMPONENT_NAME__ utilities', () => {
|
|
6
6
|
it('trims whitespace from label', () => {
|
|
7
|
-
expect(
|
|
7
|
+
expect(format__COMPONENT_NAME_PASCAL__Label(' hello ')).toBe('hello');
|
|
8
8
|
});
|
|
9
9
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock journey step for the __COMPONENT_NAME__ stage's Storybook stories.
|
|
3
|
+
*
|
|
4
|
+
* A stage receives a full JourneyStep with one or more callbacks. Replace the
|
|
5
|
+
* placeholder callback entries below with the real shape AM sends for the
|
|
6
|
+
* page node this stage renders. Add more callbacks if your stage composes
|
|
7
|
+
* multiple inputs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createJourneyStep } from '$login-framework';
|
|
11
|
+
|
|
12
|
+
export default createJourneyStep({
|
|
13
|
+
authId: 'test-auth-id',
|
|
14
|
+
callbacks: [
|
|
15
|
+
{
|
|
16
|
+
type: 'NameCallback',
|
|
17
|
+
output: [{ name: 'prompt', value: 'User Name' }],
|
|
18
|
+
input: [{ name: 'IDToken1', value: '' }],
|
|
19
|
+
_id: 0,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
stage: '__COMPONENT_NAME__',
|
|
23
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storybook stories for the __COMPONENT_NAME__ stage.
|
|
3
|
+
*
|
|
4
|
+
* Add additional stories for variants (loading, with form error, etc.) by
|
|
5
|
+
* cloning the `Base` args and overriding the relevant fields.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { fn } from 'storybook/test';
|
|
9
|
+
import { writable } from 'svelte/store';
|
|
10
|
+
|
|
11
|
+
import step from './__COMPONENT_SLUG__.mock';
|
|
12
|
+
import Story from './__COMPONENT_SLUG__.story.svelte';
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
argTypes: {
|
|
16
|
+
form: { control: false },
|
|
17
|
+
journey: { control: false },
|
|
18
|
+
step: { control: false },
|
|
19
|
+
},
|
|
20
|
+
component: Story,
|
|
21
|
+
parameters: {
|
|
22
|
+
layout: 'fullscreen',
|
|
23
|
+
},
|
|
24
|
+
title: 'Custom/Stage/__COMPONENT_NAME__',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const Base = {
|
|
28
|
+
args: {
|
|
29
|
+
form: {
|
|
30
|
+
icon: true,
|
|
31
|
+
message: '',
|
|
32
|
+
status: '',
|
|
33
|
+
submit: fn(),
|
|
34
|
+
},
|
|
35
|
+
journey: {
|
|
36
|
+
loading: false,
|
|
37
|
+
pop: fn(),
|
|
38
|
+
push: fn(),
|
|
39
|
+
stack: writable([]),
|
|
40
|
+
},
|
|
41
|
+
step,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Story wrapper for the __COMPONENT_NAME__ stage. Builds the metadata structure
|
|
3
|
+
the stage expects and mounts the component inside <Centered> so it can render
|
|
4
|
+
in isolation inside Storybook.
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import {
|
|
9
|
+
buildCallbackMetadata,
|
|
10
|
+
buildStepMetadata,
|
|
11
|
+
Centered,
|
|
12
|
+
initCheckValidation,
|
|
13
|
+
initializeLinks,
|
|
14
|
+
initializeStyles,
|
|
15
|
+
type JourneyStep,
|
|
16
|
+
type StageFormObject,
|
|
17
|
+
type StageJourneyObject,
|
|
18
|
+
type StyleObject,
|
|
19
|
+
} from '$login-framework';
|
|
20
|
+
import __COMPONENT_NAME_PASCAL__ from './__COMPONENT_SLUG__.svelte';
|
|
21
|
+
|
|
22
|
+
export let form: StageFormObject;
|
|
23
|
+
export let journey: StageJourneyObject;
|
|
24
|
+
export let step: JourneyStep;
|
|
25
|
+
export let style: StyleObject = {};
|
|
26
|
+
|
|
27
|
+
const callbackMetadata = buildCallbackMetadata(step, initCheckValidation());
|
|
28
|
+
const stepMetadata = buildStepMetadata(callbackMetadata, undefined, step.getStage());
|
|
29
|
+
const metadata = {
|
|
30
|
+
callbacks: callbackMetadata,
|
|
31
|
+
step: stepMetadata,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
initializeLinks({ termsAndConditions: '/' });
|
|
35
|
+
initializeStyles(style);
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<Centered>
|
|
39
|
+
<svelte:component
|
|
40
|
+
this={__COMPONENT_NAME_PASCAL__}
|
|
41
|
+
componentStyle="modal"
|
|
42
|
+
{form}
|
|
43
|
+
{journey}
|
|
44
|
+
{metadata}
|
|
45
|
+
{step}
|
|
46
|
+
/>
|
|
47
|
+
</Centered>
|
|
@@ -6,7 +6,7 @@ Name: __COMPONENT_NAME__
|
|
|
6
6
|
Custom stage component. Replace this description with your own.
|
|
7
7
|
|
|
8
8
|
A stage controls the layout and submission behaviour of an entire authentication
|
|
9
|
-
step (page node). It receives the full
|
|
9
|
+
step (page node). It receives the full JourneyStep, maps each callback to its
|
|
10
10
|
component via CallbackMapper, and renders the form chrome (header, alerts,
|
|
11
11
|
submit button, links).
|
|
12
12
|
-->
|
|
@@ -15,27 +15,28 @@ submit button, links).
|
|
|
15
15
|
import { afterUpdate, onDestroy, onMount } from 'svelte';
|
|
16
16
|
import { get } from 'svelte/store';
|
|
17
17
|
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
import type { styleSchema } from '$core/style.store';
|
|
18
|
+
import {
|
|
19
|
+
Alert,
|
|
20
|
+
Button,
|
|
21
|
+
CallbackMapper,
|
|
22
|
+
captureLinks,
|
|
23
|
+
convertStringToKey,
|
|
24
|
+
Form,
|
|
25
|
+
interpolate,
|
|
26
|
+
styleStore,
|
|
27
|
+
T,
|
|
28
|
+
} from '$login-framework';
|
|
29
|
+
|
|
30
|
+
import type { JourneyStep } from '@forgerock/journey-client/types';
|
|
31
|
+
|
|
33
32
|
import type {
|
|
34
33
|
CallbackMetadata,
|
|
34
|
+
Maybe,
|
|
35
35
|
StageFormObject,
|
|
36
36
|
StageJourneyObject,
|
|
37
37
|
StepMetadata,
|
|
38
|
-
|
|
38
|
+
StyleObject,
|
|
39
|
+
} from '$login-framework';
|
|
39
40
|
|
|
40
41
|
/** Display mode — determines which chrome is visible (header, links, etc.). */
|
|
41
42
|
export let componentStyle: 'app' | 'inline' | 'modal';
|
|
@@ -52,11 +53,11 @@ submit button, links).
|
|
|
52
53
|
/** Step + callback metadata from AM — policies, derived helpers, stage header. */
|
|
53
54
|
export let metadata: Maybe<{ callbacks: CallbackMetadata[]; step: StepMetadata }>;
|
|
54
55
|
|
|
55
|
-
/** The raw
|
|
56
|
-
export let step:
|
|
56
|
+
/** The raw JourneyStep from Journey Client — contains all callbacks for this step. */
|
|
57
|
+
export let step: JourneyStep;
|
|
57
58
|
|
|
58
59
|
// Subscribe to style store so the template can pass styles to child callbacks.
|
|
59
|
-
let currentStyle:
|
|
60
|
+
let currentStyle: StyleObject = get(styleStore);
|
|
60
61
|
const unsubStyle = styleStore.subscribe((v) => (currentStyle = v));
|
|
61
62
|
onDestroy(unsubStyle);
|
|
62
63
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { format__COMPONENT_NAME_PASCAL__Label } from './__COMPONENT_SLUG__.utilities.js';
|
|
4
4
|
|
|
5
5
|
describe('__COMPONENT_NAME__ utilities', () => {
|
|
6
6
|
it('trims whitespace from label', () => {
|
|
7
|
-
expect(
|
|
7
|
+
expect(format__COMPONENT_NAME_PASCAL__Label(' hello ')).toBe('hello');
|
|
8
8
|
});
|
|
9
9
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "IDE-only tsconfig. See companion file in this directory for details.",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"allowJs": true,
|
|
12
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
13
|
+
"paths": {
|
|
14
|
+
"$login-framework": ["../../../experimental/custom/login-framework.ts"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"include": ["**/*.ts", "**/*.svelte"]
|
|
18
|
+
}
|
package/dist/src/utils.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
/** Converts all backslashes to forward slashes for cross-platform path comparisons. */
|
|
2
2
|
export declare function normalizeSeparators(p: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Converts an arbitrary string to PascalCase, safe for use as a TypeScript identifier.
|
|
5
|
+
* Handles kebab-case, spaces, and preserves existing word boundaries in PascalCase input.
|
|
6
|
+
* Examples: "my-login-stage" → "MyLoginStage", "My Login Stage" → "MyLoginStage", "DefaultLogin" → "DefaultLogin"
|
|
7
|
+
*
|
|
8
|
+
* Intentionally duplicated from core/journey/_utilities/registry/registry.ts — tools/cli cannot
|
|
9
|
+
* depend on core/ (build-time vs. runtime boundary), so each package owns its own copy.
|
|
10
|
+
*/
|
|
11
|
+
export declare function toPascalCase(str: string): string;
|
package/dist/src/utils.js
CHANGED
|
@@ -2,3 +2,16 @@
|
|
|
2
2
|
export function normalizeSeparators(p) {
|
|
3
3
|
return p.replace(/\\/g, '/');
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Converts an arbitrary string to PascalCase, safe for use as a TypeScript identifier.
|
|
7
|
+
* Handles kebab-case, spaces, and preserves existing word boundaries in PascalCase input.
|
|
8
|
+
* Examples: "my-login-stage" → "MyLoginStage", "My Login Stage" → "MyLoginStage", "DefaultLogin" → "DefaultLogin"
|
|
9
|
+
*
|
|
10
|
+
* Intentionally duplicated from core/journey/_utilities/registry/registry.ts — tools/cli cannot
|
|
11
|
+
* depend on core/ (build-time vs. runtime boundary), so each package owns its own copy.
|
|
12
|
+
*/
|
|
13
|
+
export function toPascalCase(str) {
|
|
14
|
+
return str
|
|
15
|
+
.replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase())
|
|
16
|
+
.replace(/^(.)/, (_, chr) => chr.toUpperCase());
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgerock/login-framework-cli",
|
|
3
|
-
"version": "0.0.0-beta-
|
|
3
|
+
"version": "0.0.0-beta-20260527211328",
|
|
4
4
|
"description": "CLI tool for scaffolding and managing Ping Login Widget and Login App custom component projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
|
-
".": "./dist/src/main.js"
|
|
8
|
-
"./generate-registry": "./dist/scripts/generate-registry.js"
|
|
7
|
+
".": "./dist/src/main.js"
|
|
9
8
|
},
|
|
10
9
|
"main": "./dist/src/main.js",
|
|
11
10
|
"module": "./dist/src/main.js",
|
|
@@ -44,6 +43,7 @@
|
|
|
44
43
|
"@effect/printer-ansi": "^0.49.0",
|
|
45
44
|
"@effect/typeclass": "^0.40.0",
|
|
46
45
|
"effect": "^3.21.0",
|
|
46
|
+
"svelte": "^5.55.7",
|
|
47
47
|
"tar": "^7.5.13"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Thin Effect runner for generating the custom component registry.
|
|
4
|
-
*
|
|
5
|
-
* Called by:
|
|
6
|
-
* - packages/login-widget/package.json "prebuild"
|
|
7
|
-
* - Root package.json "generate:registry"
|
|
8
|
-
*
|
|
9
|
-
* Requires the CLI to be compiled first:
|
|
10
|
-
* pnpm --filter @forgerock/login-framework-cli run build
|
|
11
|
-
*/
|
|
12
|
-
import { NodeContext, NodeRuntime } from '@effect/platform-node';
|
|
13
|
-
import { Effect } from 'effect';
|
|
14
|
-
const { runRegistryScript } = await import('../src/services/registry.js');
|
|
15
|
-
const projectDir = process.argv[2];
|
|
16
|
-
if (!projectDir) {
|
|
17
|
-
console.error('Usage: node generate-registry.mjs <projectDir>');
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
NodeRuntime.runMain(runRegistryScript(projectDir).pipe(Effect.provide(NodeContext.layer)));
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { FileSystem, Path } from '@effect/platform';
|
|
2
|
-
import { Effect } from 'effect';
|
|
3
|
-
import { RegistryScanError } from '../errors.js';
|
|
4
|
-
type ComponentType = 'stage' | 'callback';
|
|
5
|
-
/** Extracts names of all `export let` prop declarations from a Svelte component's `<script>` block. */
|
|
6
|
-
export declare function parseAcceptedProps(content: string): string[];
|
|
7
|
-
/** Parses and validates the leading `<!-- @component -->` block from a Svelte file. */
|
|
8
|
-
export declare const parseComponentHeader: (filePath: string, content: string) => Effect.Effect<{
|
|
9
|
-
type: ComponentType;
|
|
10
|
-
name: string;
|
|
11
|
-
}, RegistryScanError>;
|
|
12
|
-
/**
|
|
13
|
-
* Scans `experimental/custom/stages/` and `experimental/custom/callbacks/` for
|
|
14
|
-
* `@component`-annotated Svelte files and writes
|
|
15
|
-
* `core/journey/_utilities/custom-registry.ts`.
|
|
16
|
-
*
|
|
17
|
-
* All I/O runs in-process via the platform `FileSystem` service — no subprocess
|
|
18
|
-
* spawning. Validation errors across multiple components are collected and
|
|
19
|
-
* reported together.
|
|
20
|
-
*/
|
|
21
|
-
export declare const runRegistryScript: (projectDir: string) => Effect.Effect<void, RegistryScanError, FileSystem.FileSystem | Path.Path>;
|
|
22
|
-
export {};
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { FileSystem, Path } from '@effect/platform';
|
|
2
|
-
import { Console, Effect } from 'effect';
|
|
3
|
-
import { RegistryScanError } from '../errors.js';
|
|
4
|
-
// --------------------------------------------------------------------------
|
|
5
|
-
// Helpers (exported for testing)
|
|
6
|
-
// --------------------------------------------------------------------------
|
|
7
|
-
/** Extracts names of all `export let` prop declarations from a Svelte component's `<script>` block. */
|
|
8
|
-
export function parseAcceptedProps(content) {
|
|
9
|
-
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
10
|
-
if (!scriptMatch)
|
|
11
|
-
return [];
|
|
12
|
-
const props = [];
|
|
13
|
-
const regex = /export\s+let\s+(\w+)/g;
|
|
14
|
-
let m;
|
|
15
|
-
while ((m = regex.exec(scriptMatch[1])) !== null)
|
|
16
|
-
props.push(m[1]);
|
|
17
|
-
return props;
|
|
18
|
-
}
|
|
19
|
-
function toPascalCase(str) {
|
|
20
|
-
return str
|
|
21
|
-
.replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase())
|
|
22
|
-
.replace(/^(.)/, (_, chr) => chr.toUpperCase());
|
|
23
|
-
}
|
|
24
|
-
/** Parses and validates the leading `<!-- @component -->` block from a Svelte file. */
|
|
25
|
-
export const parseComponentHeader = (filePath, content) => {
|
|
26
|
-
const fail = (cause) => Effect.fail(new RegistryScanError({ directory: filePath, cause }));
|
|
27
|
-
const commentMatch = content.match(/^<!--([\s\S]*?)-->/);
|
|
28
|
-
if (!commentMatch)
|
|
29
|
-
return fail('Missing @component header. Every custom component must begin with:\n' +
|
|
30
|
-
'<!--\n @component\n Type: stage|callback\n Name: <ComponentName>\n -->');
|
|
31
|
-
const block = commentMatch[1];
|
|
32
|
-
if (!block.includes('@component'))
|
|
33
|
-
return fail('Missing "@component" tag in the opening comment.');
|
|
34
|
-
const typeMatch = block.match(/Type:\s*(\S+)/);
|
|
35
|
-
if (!typeMatch)
|
|
36
|
-
return fail('Missing "Type:" field in @component header. Expected: Type: stage or Type: callback');
|
|
37
|
-
const rawType = typeMatch[1].toLowerCase();
|
|
38
|
-
if (rawType !== 'stage' && rawType !== 'callback')
|
|
39
|
-
return fail(`Invalid Type value "${typeMatch[1]}". Must be "stage" or "callback".`);
|
|
40
|
-
const nameMatch = block.match(/Name:\s*(.+)/);
|
|
41
|
-
if (!nameMatch)
|
|
42
|
-
return fail('Missing "Name:" field in @component header. Expected: Name: <ComponentName>');
|
|
43
|
-
return Effect.succeed({ type: rawType, name: nameMatch[1].trim() });
|
|
44
|
-
};
|
|
45
|
-
// --------------------------------------------------------------------------
|
|
46
|
-
// File scanning
|
|
47
|
-
// --------------------------------------------------------------------------
|
|
48
|
-
const findSvelteFiles = (fs, path, dir) => fs.exists(dir).pipe(Effect.orElseSucceed(() => false), Effect.flatMap((exists) => !exists
|
|
49
|
-
? Effect.succeed([])
|
|
50
|
-
: fs.readDirectory(dir).pipe(Effect.mapError((cause) => new RegistryScanError({ directory: dir, cause })), Effect.flatMap((entries) => Effect.forEach(entries, (entry) => {
|
|
51
|
-
const fullPath = path.join(dir, entry);
|
|
52
|
-
return fs.stat(fullPath).pipe(Effect.mapError((cause) => new RegistryScanError({ directory: dir, cause })), Effect.flatMap((stat) => stat.type === 'Directory'
|
|
53
|
-
? findSvelteFiles(fs, path, fullPath)
|
|
54
|
-
: Effect.succeed(entry.endsWith('.svelte') && !entry.endsWith('.story.svelte')
|
|
55
|
-
? [fullPath]
|
|
56
|
-
: [])));
|
|
57
|
-
}, { concurrency: 'unbounded' })), Effect.map((nested) => nested.flat()))));
|
|
58
|
-
const scanDirectory = (fs, path, dir, expectedType) => findSvelteFiles(fs, path, dir).pipe(Effect.flatMap((files) => Effect.validateAll(files, (filePath) => fs.readFileString(filePath).pipe(Effect.mapError((cause) => new RegistryScanError({ directory: filePath, cause })), Effect.flatMap((content) => parseComponentHeader(filePath, content).pipe(Effect.flatMap(({ type, name }) => type !== expectedType
|
|
59
|
-
? Effect.fail(new RegistryScanError({
|
|
60
|
-
directory: filePath,
|
|
61
|
-
cause: `Component in /experimental/custom/${expectedType}s/ declares Type: "${type}". Must declare Type: ${expectedType}`,
|
|
62
|
-
}))
|
|
63
|
-
: Effect.succeed({
|
|
64
|
-
filePath,
|
|
65
|
-
name,
|
|
66
|
-
type,
|
|
67
|
-
acceptedProps: parseAcceptedProps(content),
|
|
68
|
-
})))))).pipe(Effect.mapError((errors) => new RegistryScanError({
|
|
69
|
-
directory: dir,
|
|
70
|
-
cause: errors.map((e) => String(e.cause)).join('\n'),
|
|
71
|
-
})))));
|
|
72
|
-
// --------------------------------------------------------------------------
|
|
73
|
-
// Registry content builder (pure)
|
|
74
|
-
// --------------------------------------------------------------------------
|
|
75
|
-
function buildRegistryContent(path, registryDir, stageComponents, callbackComponents) {
|
|
76
|
-
const toEntry = (prefix) => ({ filePath, name, acceptedProps }) => {
|
|
77
|
-
const relPath = path.relative(registryDir, filePath).replace(/\\/g, '/');
|
|
78
|
-
const importPath = relPath.startsWith('.') ? relPath : `./${relPath}`;
|
|
79
|
-
return { varName: `${prefix}${toPascalCase(name)}`, importPath, name, acceptedProps };
|
|
80
|
-
};
|
|
81
|
-
const stageEntries = stageComponents.map(toEntry('Stage'));
|
|
82
|
-
const callbackEntries = callbackComponents.map(toEntry('Callback'));
|
|
83
|
-
const lines = [
|
|
84
|
-
`/**`,
|
|
85
|
-
` * AUTO-GENERATED — do not edit by hand.`,
|
|
86
|
-
` * Run \`pnpm build:widget\` or \`pnpm --filter @forgerock/login-widget exec vite build\` to regenerate.`,
|
|
87
|
-
` *`,
|
|
88
|
-
` * Source: /experimental/custom/stages/ and /experimental/custom/callbacks/`,
|
|
89
|
-
` */`,
|
|
90
|
-
``,
|
|
91
|
-
`import type { Component } from 'svelte';`,
|
|
92
|
-
``,
|
|
93
|
-
`export interface CustomRegistryEntry {`,
|
|
94
|
-
` component: Component;`,
|
|
95
|
-
` /** Props declared via \`export let\` in the component — only these are forwarded by the mapper. */`,
|
|
96
|
-
` acceptedProps: string[];`,
|
|
97
|
-
`}`,
|
|
98
|
-
``,
|
|
99
|
-
];
|
|
100
|
-
if (stageEntries.length > 0) {
|
|
101
|
-
lines.push(`// Stage overrides / extensions`);
|
|
102
|
-
for (const { varName, importPath } of stageEntries)
|
|
103
|
-
lines.push(`import ${varName} from '${importPath}';`);
|
|
104
|
-
lines.push(``);
|
|
105
|
-
}
|
|
106
|
-
if (callbackEntries.length > 0) {
|
|
107
|
-
lines.push(`// Callback overrides / extensions`);
|
|
108
|
-
for (const { varName, importPath } of callbackEntries)
|
|
109
|
-
lines.push(`import ${varName} from '${importPath}';`);
|
|
110
|
-
lines.push(``);
|
|
111
|
-
}
|
|
112
|
-
lines.push(`export const customStageRegistry: Record<string, CustomRegistryEntry> = {`);
|
|
113
|
-
for (const { varName, name, acceptedProps } of stageEntries)
|
|
114
|
-
lines.push(` ${JSON.stringify(name)}: { component: ${varName}, acceptedProps: ${JSON.stringify(acceptedProps)} },`);
|
|
115
|
-
lines.push(`};`);
|
|
116
|
-
lines.push(``);
|
|
117
|
-
lines.push(`export const customCallbackRegistry: Record<string, CustomRegistryEntry> = {`);
|
|
118
|
-
for (const { varName, name, acceptedProps } of callbackEntries)
|
|
119
|
-
lines.push(` ${JSON.stringify(name)}: { component: ${varName}, acceptedProps: ${JSON.stringify(acceptedProps)} },`);
|
|
120
|
-
lines.push(`};`);
|
|
121
|
-
lines.push(``);
|
|
122
|
-
return lines.join('\n');
|
|
123
|
-
}
|
|
124
|
-
// --------------------------------------------------------------------------
|
|
125
|
-
// Public API
|
|
126
|
-
// --------------------------------------------------------------------------
|
|
127
|
-
/**
|
|
128
|
-
* Scans `experimental/custom/stages/` and `experimental/custom/callbacks/` for
|
|
129
|
-
* `@component`-annotated Svelte files and writes
|
|
130
|
-
* `core/journey/_utilities/custom-registry.ts`.
|
|
131
|
-
*
|
|
132
|
-
* All I/O runs in-process via the platform `FileSystem` service — no subprocess
|
|
133
|
-
* spawning. Validation errors across multiple components are collected and
|
|
134
|
-
* reported together.
|
|
135
|
-
*/
|
|
136
|
-
export const runRegistryScript = (projectDir) => Effect.gen(function* () {
|
|
137
|
-
const fs = yield* FileSystem.FileSystem;
|
|
138
|
-
const path = yield* Path.Path;
|
|
139
|
-
const stageDir = path.join(projectDir, 'experimental', 'custom', 'stages');
|
|
140
|
-
const callbackDir = path.join(projectDir, 'experimental', 'custom', 'callbacks');
|
|
141
|
-
const registryDir = path.join(projectDir, 'core', 'journey', '_utilities');
|
|
142
|
-
const registryPath = path.join(registryDir, 'custom-registry.ts');
|
|
143
|
-
const [stageComponents, callbackComponents] = yield* Effect.all([
|
|
144
|
-
scanDirectory(fs, path, stageDir, 'stage'),
|
|
145
|
-
scanDirectory(fs, path, callbackDir, 'callback'),
|
|
146
|
-
], { concurrency: 'unbounded' });
|
|
147
|
-
const content = buildRegistryContent(path, registryDir, stageComponents, callbackComponents);
|
|
148
|
-
yield* fs
|
|
149
|
-
.makeDirectory(registryDir, { recursive: true })
|
|
150
|
-
.pipe(Effect.mapError((cause) => new RegistryScanError({ directory: registryDir, cause })));
|
|
151
|
-
yield* fs
|
|
152
|
-
.writeFileString(registryPath, content)
|
|
153
|
-
.pipe(Effect.mapError((cause) => new RegistryScanError({ directory: registryPath, cause })));
|
|
154
|
-
const total = stageComponents.length + callbackComponents.length;
|
|
155
|
-
if (total === 0) {
|
|
156
|
-
yield* Console.log(`custom-registry.ts generated (no custom components found — registries are empty)`);
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
yield* Console.log(`custom-registry.ts generated:`);
|
|
160
|
-
if (stageComponents.length > 0)
|
|
161
|
-
yield* Console.log(` Stages (${stageComponents.length}): ${stageComponents
|
|
162
|
-
.map((c) => c.name)
|
|
163
|
-
.join(', ')}`);
|
|
164
|
-
if (callbackComponents.length > 0)
|
|
165
|
-
yield* Console.log(` Callbacks (${callbackComponents.length}): ${callbackComponents
|
|
166
|
-
.map((c) => c.name)
|
|
167
|
-
.join(', ')}`);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility functions for the __COMPONENT_NAME__ callback.
|
|
3
|
-
*
|
|
4
|
-
* Keep callback-specific helpers here — data formatting, validation, API calls, etc.
|
|
5
|
-
* This file is unit-tested independently of the Svelte component.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Example utility — replace with your own helpers.
|
|
9
|
-
*/
|
|
10
|
-
export declare function format__COMPONENT_NAME__Label(value: string): string;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility functions for the __COMPONENT_NAME__ callback.
|
|
3
|
-
*
|
|
4
|
-
* Keep callback-specific helpers here — data formatting, validation, API calls, etc.
|
|
5
|
-
* This file is unit-tested independently of the Svelte component.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Example utility — replace with your own helpers.
|
|
9
|
-
*/
|
|
10
|
-
export function format__COMPONENT_NAME__Label(value) {
|
|
11
|
-
return value.trim();
|
|
12
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { format__COMPONENT_NAME__Label } from './__COMPONENT_SLUG__.utilities.js';
|
|
3
|
-
describe('__COMPONENT_NAME__ utilities', () => {
|
|
4
|
-
it('trims whitespace from label', () => {
|
|
5
|
-
expect(format__COMPONENT_NAME__Label(' hello ')).toBe('hello');
|
|
6
|
-
});
|
|
7
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility functions for the __COMPONENT_NAME__ stage.
|
|
3
|
-
*
|
|
4
|
-
* Keep stage-specific helpers here — data formatting, validation, API calls, etc.
|
|
5
|
-
* This file is unit-tested independently of the Svelte component.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Example utility — replace with your own helpers.
|
|
9
|
-
*/
|
|
10
|
-
export declare function format__COMPONENT_NAME__Label(value: string): string;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility functions for the __COMPONENT_NAME__ stage.
|
|
3
|
-
*
|
|
4
|
-
* Keep stage-specific helpers here — data formatting, validation, API calls, etc.
|
|
5
|
-
* This file is unit-tested independently of the Svelte component.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Example utility — replace with your own helpers.
|
|
9
|
-
*/
|
|
10
|
-
export function format__COMPONENT_NAME__Label(value) {
|
|
11
|
-
return value.trim();
|
|
12
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { format__COMPONENT_NAME__Label } from './__COMPONENT_SLUG__.utilities.js';
|
|
3
|
-
describe('__COMPONENT_NAME__ utilities', () => {
|
|
4
|
-
it('trims whitespace from label', () => {
|
|
5
|
-
expect(format__COMPONENT_NAME__Label(' hello ')).toBe('hello');
|
|
6
|
-
});
|
|
7
|
-
});
|