@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.
Files changed (40) hide show
  1. package/README.md +40 -28
  2. package/dist/src/commands/generate.d.ts +1 -1
  3. package/dist/src/commands/generate.js +11 -8
  4. package/dist/src/commands/init.d.ts +2 -2
  5. package/dist/src/commands/init.js +5 -15
  6. package/dist/src/commands/update.d.ts +2 -2
  7. package/dist/src/commands/update.js +4 -10
  8. package/dist/src/config/version.d.ts +0 -3
  9. package/dist/src/config/version.js +0 -1
  10. package/dist/src/errors.d.ts +0 -8
  11. package/dist/src/errors.js +0 -2
  12. package/dist/src/main.js +2 -2
  13. package/dist/src/templates/callback/__COMPONENT_SLUG__.mock.ts +22 -0
  14. package/dist/src/templates/callback/__COMPONENT_SLUG__.stories.js +28 -0
  15. package/dist/src/templates/callback/__COMPONENT_SLUG__.story.svelte +59 -0
  16. package/dist/src/templates/callback/__COMPONENT_SLUG__.svelte +10 -36
  17. package/dist/src/templates/callback/__COMPONENT_SLUG__.utilities.test.ts +2 -2
  18. package/dist/src/templates/callback/__COMPONENT_SLUG__.utilities.ts +1 -1
  19. package/dist/src/templates/stage/__COMPONENT_SLUG__.mock.ts +23 -0
  20. package/dist/src/templates/stage/__COMPONENT_SLUG__.stories.js +43 -0
  21. package/dist/src/templates/stage/__COMPONENT_SLUG__.story.svelte +47 -0
  22. package/dist/src/templates/stage/__COMPONENT_SLUG__.svelte +21 -20
  23. package/dist/src/templates/stage/__COMPONENT_SLUG__.utilities.test.ts +2 -2
  24. package/dist/src/templates/stage/__COMPONENT_SLUG__.utilities.ts +1 -1
  25. package/dist/src/templates/tsconfig.json +18 -0
  26. package/dist/src/utils.d.ts +9 -0
  27. package/dist/src/utils.js +13 -0
  28. package/package.json +3 -3
  29. package/dist/scripts/generate-registry.d.ts +0 -2
  30. package/dist/scripts/generate-registry.js +0 -20
  31. package/dist/src/services/registry.d.ts +0 -22
  32. package/dist/src/services/registry.js +0 -169
  33. package/dist/templates/callback/__COMPONENT_SLUG__.utilities.d.ts +0 -10
  34. package/dist/templates/callback/__COMPONENT_SLUG__.utilities.js +0 -12
  35. package/dist/templates/callback/__COMPONENT_SLUG__.utilities.test.d.ts +0 -1
  36. package/dist/templates/callback/__COMPONENT_SLUG__.utilities.test.js +0 -7
  37. package/dist/templates/stage/__COMPONENT_SLUG__.utilities.d.ts +0 -10
  38. package/dist/templates/stage/__COMPONENT_SLUG__.utilities.js +0 -12
  39. package/dist/templates/stage/__COMPONENT_SLUG__.utilities.test.d.ts +0 -1
  40. 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 | Description |
39
- | ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
40
- | `--version <tag>` | Download a specific release (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. |
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. Seeds an empty `core/journey/_utilities/custom-registry.ts`
51
- 7. Injects the custom registry Vite plugin into widget and app configs
52
- 8. Writes `.generator-version` to track the framework version in use
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>/`, then regenerates `custom-registry.ts`.
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 pre-build script and `ping-lf` both read this header to register the component:
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. Regenerates the component registry after the update.
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 --version v1.5.0 # pin to a specific version
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
- > **All commands below must be run from the `tools/cli/` directory.**
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
- ```sh
145
- cd tools/cli
144
+ ### Recommended setup (once per machine)
146
145
 
147
- # Install dependencies
148
- pnpm install
146
+ From the repo root, after `pnpm install`:
149
147
 
150
- # Build the CLI
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
- # Run tests
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
- # Watch mode (rebuild on file changes)
157
- pnpm dev
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
- ### Running locally without installing globally
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
- # From tools/cli/ after pnpm build:
164
- node dist/main.js init my-project
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
- # Or from anywhere using the full path:
167
- node /path/to/forgerock-web-login-framework/tools/cli/dist/main.js init my-project
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 | import("../errors.js").RegistryScanError | InvalidComponentNameError | ComponentAlreadyExistsError | import("@effect/platform/Error").PlatformError | import("@effect/platform/Terminal").QuitException, {
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 { runRegistryScript } from '../services/registry.js';
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 contain both
89
- // __COMPONENT_NAME__ (PascalCase) and __COMPONENT_SLUG__ (kebab-case).
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((f) => ` ${f}`).join('\n') +
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 | import("../errors.js").RegistryScanError | DirectoryConflictError | DirectoryNotEmptyError | import("@effect/platform/Error").PlatformError, {
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 version: import("effect/Option").Option<string>;
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
- version: Options.optional(Options.text('version').pipe(Options.withDescription('Framework version tag to download (e.g. v1.2.0). If omitted, the main branch is used.'))),
38
- }, ({ directory, local, version }) => Effect.gen(function* () {
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, version, resolvedDir);
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. Generate custom-registry.ts ────────────────────────────────────
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
- // ── 7. Print next steps ───────────────────────────────────────────────
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("../errors.js").RegistryScanError | import("@effect/platform/Error").PlatformError, {
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 version: import("effect/Option").Option<string>;
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
- version: Options.optional(Options.text('version').pipe(Options.withDescription('Framework version tag to update to (e.g. v1.2.0). If omitted, the main branch is used.'))),
10
- }, ({ local, version }) => Effect.gen(function* () {
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, version, cwd);
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. Regenerate custom-registry.ts ──────────────────────────────────
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';
@@ -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>;
@@ -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)('../package.json');
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 version: ping-lf init <dir> --version 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` +
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 { FRCallback } from '@forgerock/javascript-sdk';
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 AM callback instance for this component. Use `callback.getInputValue()`
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: FRCallback;
26
-
27
- /**
28
- * Optional function to trigger self-submission (e.g. after an async action).
29
- * Set to `null` when the callback does not need to submit the form itself.
30
- */
31
- export const selfSubmitFunction: Maybe<SelfSubmitFunction> = null;
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 { format__COMPONENT_NAME__Label } from './__COMPONENT_SLUG__.utilities.js';
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(format__COMPONENT_NAME__Label(' hello ')).toBe('hello');
7
+ expect(format__COMPONENT_NAME_PASCAL__Label(' hello ')).toBe('hello');
8
8
  });
9
9
  });
@@ -8,6 +8,6 @@
8
8
  /**
9
9
  * Example utility — replace with your own helpers.
10
10
  */
11
- export function format__COMPONENT_NAME__Label(value: string): string {
11
+ export function format__COMPONENT_NAME_PASCAL__Label(value: string): string {
12
12
  return value.trim();
13
13
  }
@@ -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 FRStep, maps each callback to its
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 T from '$components/_utilities/locale-strings.svelte';
19
- import Alert from '$components/primitives/alert/alert.svelte';
20
- import Button from '$components/primitives/button/button.svelte';
21
- import Form from '$components/primitives/form/form.svelte';
22
- import { interpolate } from '$core/_utilities/i18n.utilities';
23
- import { styleStore } from '$core/style.store';
24
- import CallbackMapper from '$journey/_utilities/callback-mapper.svelte';
25
- import { captureLinks } from '$journey/stages/_utilities/stage.utilities';
26
- import { convertStringToKey } from '$journey/stages/_utilities/step.utilities';
27
-
28
- import type { FRStep } from '@forgerock/javascript-sdk';
29
- import type { z } from 'zod';
30
-
31
- import type { Maybe } from '$core/interfaces';
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
- } from '$journey/journey.interfaces';
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 FRStep from the JavaScript SDK — contains all callbacks for this step. */
56
- export let step: FRStep;
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: z.infer<typeof styleSchema> = get(styleStore);
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 { format__COMPONENT_NAME__Label } from './__COMPONENT_SLUG__.utilities.js';
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(format__COMPONENT_NAME__Label(' hello ')).toBe('hello');
7
+ expect(format__COMPONENT_NAME_PASCAL__Label(' hello ')).toBe('hello');
8
8
  });
9
9
  });
@@ -8,6 +8,6 @@
8
8
  /**
9
9
  * Example utility — replace with your own helpers.
10
10
  */
11
- export function format__COMPONENT_NAME__Label(value: string): string {
11
+ export function format__COMPONENT_NAME_PASCAL__Label(value: string): string {
12
12
  return value.trim();
13
13
  }
@@ -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
+ }
@@ -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-20260527205941",
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,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
@@ -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,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,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
- });