@forgerock/login-framework-cli 0.0.0-beta-20261013223421 → 0.0.0-beta-20261021173425
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 +8 -3
- package/dist/src/commands/generate.d.ts +12 -2
- package/dist/src/commands/generate.js +10 -8
- package/dist/src/commands/init.d.ts +7 -2
- package/dist/src/commands/init.js +17 -12
- package/dist/src/commands/update.d.ts +8 -2
- package/dist/src/commands/update.js +20 -13
- package/dist/src/errors.d.ts +0 -8
- package/dist/src/errors.js +0 -2
- package/dist/src/mcp.js +0 -4
- package/dist/src/services/file-system.d.ts +32 -1
- package/dist/src/services/file-system.js +64 -2
- package/dist/src/templates/wrapper/footer.svelte +68 -0
- package/dist/src/templates/wrapper/header.svelte +67 -0
- package/package.json +2 -2
- package/dist/src/services/registry.d.ts +0 -29
- package/dist/src/services/registry.js +0 -176
package/README.md
CHANGED
|
@@ -45,11 +45,11 @@ pnpm dev
|
|
|
45
45
|
1. Downloads the framework archive from GitHub Releases (or copies from `--local`)
|
|
46
46
|
2. Copies framework files, excluding build artifacts, secrets, and `tools/`
|
|
47
47
|
3. Writes a minimal `pnpm-workspace.yaml`
|
|
48
|
-
4. Creates `experimental/custom/callbacks/` and `experimental/custom/stages/` with `.gitkeep` placeholders
|
|
48
|
+
4. Creates `experimental/custom/callbacks/` and `experimental/custom/stages/` with `.gitkeep` placeholders, and places the page header and footer wrapper files `experimental/custom/header/header.svelte` and `experimental/custom/footer/footer.svelte`
|
|
49
49
|
5. Copies the component authoring guide to `experimental/custom/README.md`
|
|
50
50
|
6. Writes `.generator-version` to track the framework version in use
|
|
51
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
|
|
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 generates it. (With `--local`, the bulk copy carries over whatever `custom-registry.ts` exists in the local source; the Vite plugin regenerates it on the next build.)
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
@@ -93,6 +93,8 @@ Name: NameCallback
|
|
|
93
93
|
|
|
94
94
|
Setting `Name` to an existing default (e.g. `NameCallback`, `DefaultLogin`) **overrides** that component. Setting it to a new name **extends** the framework with a custom type.
|
|
95
95
|
|
|
96
|
+
Page headers and footers are not scaffolded with `generate`: they are fixed wrapper files (`experimental/custom/header/header.svelte` and `experimental/custom/footer/footer.svelte`), generated on first build by the framework's Vite plugin and never overwritten once they exist. Edit the wrappers directly.
|
|
97
|
+
|
|
96
98
|
See `experimental/custom/README.md` for the full prop contract.
|
|
97
99
|
|
|
98
100
|
---
|
|
@@ -171,6 +173,7 @@ ping-lf update --directory /path/to/project # target a specific project root
|
|
|
171
173
|
**What it preserves**
|
|
172
174
|
|
|
173
175
|
- `experimental/custom/` — all custom components are untouched
|
|
176
|
+
- The header and footer wrapper files are never overwritten — `update` only backfills ones that are missing, so your edits survive
|
|
174
177
|
- `pnpm-workspace.yaml` — your workspace config is not overwritten
|
|
175
178
|
- `.env` files — excluded from the copy
|
|
176
179
|
|
|
@@ -191,7 +194,9 @@ my-login-project/
|
|
|
191
194
|
│ └── custom/
|
|
192
195
|
│ ├── README.md # Component authoring guide
|
|
193
196
|
│ ├── callbacks/ # Your custom callback components
|
|
194
|
-
│
|
|
197
|
+
│ ├── stages/ # Your custom stage components
|
|
198
|
+
│ ├── header/header.svelte # Page header wrapper (placed by init; regenerated by the plugin if deleted)
|
|
199
|
+
│ └── footer/footer.svelte # Page footer wrapper (placed by init; regenerated by the plugin if deleted)
|
|
195
200
|
├── .generator-version # Tracks the framework version in use
|
|
196
201
|
└── pnpm-workspace.yaml
|
|
197
202
|
```
|
|
@@ -19,11 +19,21 @@ 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
|
-
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Resolves the templates directory relative to this file.
|
|
24
|
+
* In the compiled output (dist/src/commands/) the templates live one level up
|
|
25
|
+
* at dist/src/templates/. In Vitest (src/commands/) they live two levels up at
|
|
26
|
+
* the workspace root templates/ directory — the same fallback pattern used in mcp.ts.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getTemplatesDir(): string;
|
|
29
|
+
/** Supported custom component types for scaffolding. */
|
|
30
|
+
type ScaffoldType = 'callback' | 'stage';
|
|
31
|
+
export declare function scaffoldComponent(type: ScaffoldType, name: string, directory?: string): Effect.Effect<void, import("../errors.js").GeneratorVersionError | InvalidComponentNameError | ComponentAlreadyExistsError | import("@effect/platform/Error").PlatformError, FileSystem.FileSystem | Path.Path>;
|
|
32
|
+
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, {
|
|
24
33
|
readonly subcommand: import("effect/Option").Option<{
|
|
25
34
|
readonly name: string;
|
|
26
35
|
} | {
|
|
27
36
|
readonly name: string;
|
|
28
37
|
}>;
|
|
29
38
|
}>;
|
|
39
|
+
export {};
|
|
@@ -7,7 +7,6 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { assertValidProject } from '../config/version.js';
|
|
8
8
|
import { ComponentAlreadyExistsError, InvalidComponentNameError } from '../errors.js';
|
|
9
9
|
import { expandTilde } from '../services/file-system.js';
|
|
10
|
-
import { runRegistryScript } from '../services/registry.js';
|
|
11
10
|
import { toPascalCase } from '../utils.js';
|
|
12
11
|
const CUSTOM_DIR = 'experimental/custom';
|
|
13
12
|
/**
|
|
@@ -66,7 +65,7 @@ export const StageNameSchema = Schema.String.pipe(Schema.filter((s) => s.length
|
|
|
66
65
|
* at dist/src/templates/. In Vitest (src/commands/) they live two levels up at
|
|
67
66
|
* the workspace root templates/ directory — the same fallback pattern used in mcp.ts.
|
|
68
67
|
*/
|
|
69
|
-
function getTemplatesDir() {
|
|
68
|
+
export function getTemplatesDir() {
|
|
70
69
|
const __dirname = nodePath.dirname(fileURLToPath(import.meta.url));
|
|
71
70
|
const compiledPath = nodePath.join(__dirname, '../templates');
|
|
72
71
|
return existsSync(compiledPath) ? compiledPath : nodePath.join(__dirname, '../../templates');
|
|
@@ -77,14 +76,17 @@ export function scaffoldComponent(type, name, directory) {
|
|
|
77
76
|
const p = yield* Path.Path;
|
|
78
77
|
// ── Guard: validate component name format ─────────────────────────────
|
|
79
78
|
// Callbacks must be valid name (PascalCase, alphanumeric only).
|
|
80
|
-
// Stages are arbitrary
|
|
79
|
+
// Stages are arbitrary strings with no naming convention constraint.
|
|
81
80
|
const nameSchema = type === 'callback' ? CallbackNameSchema : StageNameSchema;
|
|
82
81
|
const { slug } = yield* Schema.decode(nameSchema)(name).pipe(Effect.mapError(() => new InvalidComponentNameError({ name })));
|
|
83
82
|
const cwd = p.resolve(expandTilde(directory ?? process.cwd()));
|
|
84
83
|
// ── Guard: must be run from an initialized project root ───────────────
|
|
85
84
|
yield* assertValidProject(cwd);
|
|
86
|
-
const
|
|
87
|
-
|
|
85
|
+
const subDirs = {
|
|
86
|
+
callback: 'callbacks',
|
|
87
|
+
stage: 'stages',
|
|
88
|
+
};
|
|
89
|
+
const componentDir = p.join(cwd, CUSTOM_DIR, subDirs[type], slug);
|
|
88
90
|
const templatesDir = p.join(getTemplatesDir(), type);
|
|
89
91
|
// ── Guard: prevent overwriting an existing component ──────────────────
|
|
90
92
|
const exists = yield* fs.exists(componentDir);
|
|
@@ -111,10 +113,10 @@ export function scaffoldComponent(type, name, directory) {
|
|
|
111
113
|
yield* fs.writeFileString(targetPath, processedContent);
|
|
112
114
|
createdFiles.push(targetPath);
|
|
113
115
|
}
|
|
114
|
-
// ── Regenerate custom-registry.ts ────────────────────────────────────
|
|
115
|
-
yield* Console.log('Regenerating custom component registry...');
|
|
116
|
-
yield* runRegistryScript(cwd);
|
|
117
116
|
// ── Print summary ──────────────────────────────────────────────────────
|
|
117
|
+
// custom-registry.ts is NOT regenerated here — the framework's Vite plugin
|
|
118
|
+
// picks the new component up on the next build or dev-server start (and,
|
|
119
|
+
// in dev, watches these directories for adds/removals without a restart).
|
|
118
120
|
yield* Console.log(`Done. ${type} component scaffolded successfully.\n\n` +
|
|
119
121
|
`Files created:\n` +
|
|
120
122
|
createdFiles.map((f) => ` ${f}`).join('\n') +
|
|
@@ -3,13 +3,18 @@ import { FileSystem, Path } from '@effect/platform';
|
|
|
3
3
|
import { Effect } from 'effect';
|
|
4
4
|
import { DirectoryConflictError, DirectoryNotEmptyError } from '../errors.js';
|
|
5
5
|
import type { Option } from 'effect';
|
|
6
|
+
/**
|
|
7
|
+
* Custom component directories scaffolded (with .gitkeep) at project init.
|
|
8
|
+
* Protected during `ping-lf update` — see PROTECTED_DIRS in services/file-system.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CUSTOM_COMPONENT_DIRS: readonly ["callbacks", "stages"];
|
|
6
11
|
export interface InitProjectOptions {
|
|
7
12
|
readonly directory: string;
|
|
8
13
|
readonly local: Option.Option<string>;
|
|
9
14
|
readonly version: Option.Option<string>;
|
|
10
15
|
}
|
|
11
|
-
export declare const initProject: ({ directory, local, version }: InitProjectOptions) => Effect.Effect<void, import("../errors.js").InvalidVersionError | import("../errors.js").ReleaseNetworkError | import("../errors.js").ReleaseParseError | import("../errors.js").ReleaseFsError | import("../errors.js").FileSystemError | DirectoryConflictError | DirectoryNotEmptyError | import("
|
|
12
|
-
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("
|
|
16
|
+
export declare const initProject: ({ directory, local, version }: InitProjectOptions) => Effect.Effect<void, 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, FileSystem.FileSystem | Path.Path | import("../services/release.js").Release>;
|
|
17
|
+
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, {
|
|
13
18
|
readonly directory: string;
|
|
14
19
|
readonly local: Option.Option<string>;
|
|
15
20
|
readonly tag: Option.Option<string>;
|
|
@@ -3,8 +3,8 @@ import { FileSystem, Path } from '@effect/platform';
|
|
|
3
3
|
import { Console, Effect } from 'effect';
|
|
4
4
|
import { writeVersion } from '../config/version.js';
|
|
5
5
|
import { DirectoryConflictError, DirectoryNotEmptyError } from '../errors.js';
|
|
6
|
-
import { copyWithExclusions, expandTilde, isFrameworkDirectory } from '../services/file-system.js';
|
|
7
|
-
import {
|
|
6
|
+
import { copyWithExclusions, copyWrapperFiles, expandTilde, isFrameworkDirectory, } from '../services/file-system.js';
|
|
7
|
+
import { getTemplatesDir } from './generate.js';
|
|
8
8
|
import { resolveSource } from './source.js';
|
|
9
9
|
/**
|
|
10
10
|
* Minimal pnpm-workspace.yaml written to the customer project.
|
|
@@ -14,6 +14,11 @@ const PNPM_WORKSPACE = `packages:
|
|
|
14
14
|
- 'apps/*'
|
|
15
15
|
- 'e2e'
|
|
16
16
|
`;
|
|
17
|
+
/**
|
|
18
|
+
* Custom component directories scaffolded (with .gitkeep) at project init.
|
|
19
|
+
* Protected during `ping-lf update` — see PROTECTED_DIRS in services/file-system.
|
|
20
|
+
*/
|
|
21
|
+
export const CUSTOM_COMPONENT_DIRS = ['callbacks', 'stages'];
|
|
17
22
|
const nextStepsMessage = (dir) => `
|
|
18
23
|
Done. Project initialized successfully.
|
|
19
24
|
|
|
@@ -57,6 +62,12 @@ export const initProject = ({ directory, local, version }) => Effect.gen(functio
|
|
|
57
62
|
yield* Console.log('Copying framework files...');
|
|
58
63
|
yield* fs.makeDirectory(resolvedDir, { recursive: true });
|
|
59
64
|
yield* copyWithExclusions(sourceDir, resolvedDir);
|
|
65
|
+
// ── 2b. Place the framework-shipped wrapper files ──────────────────
|
|
66
|
+
// The bulk copy skips them (they are file-protected: customer edits
|
|
67
|
+
// to them must survive `update`), so init places them explicitly from
|
|
68
|
+
// the CLI's own wrapper templates. The app's +page.svelte imports them
|
|
69
|
+
// directly, so a project without them cannot build.
|
|
70
|
+
yield* copyWrapperFiles(resolvedDir, getTemplatesDir());
|
|
60
71
|
return resolvedVersion;
|
|
61
72
|
}));
|
|
62
73
|
// ── 3. Write pnpm-workspace.yaml (omits tools/ present in upstream) ────
|
|
@@ -64,26 +75,20 @@ export const initProject = ({ directory, local, version }) => Effect.gen(functio
|
|
|
64
75
|
// ── 4. Scaffold experimental/custom/ ──────────────────────────────────
|
|
65
76
|
// copyWithExclusions already copies experimental/custom/demo/,
|
|
66
77
|
// login-framework.ts, README.md, and tsconfig.json from the source.
|
|
67
|
-
// Only
|
|
78
|
+
// Only the custom component dirs are protected (they hold user components).
|
|
68
79
|
// Create them as empty dirs with .gitkeep — components come via `ping-lf generate`.
|
|
69
80
|
yield* Console.log('Scaffolding custom component directories...');
|
|
70
|
-
yield* Effect.forEach(
|
|
81
|
+
yield* Effect.forEach(CUSTOM_COMPONENT_DIRS, (dir) => Effect.gen(function* () {
|
|
71
82
|
const customDir = path.join(resolvedDir, 'experimental', 'custom', dir);
|
|
72
83
|
yield* fs.makeDirectory(customDir, { recursive: true });
|
|
73
84
|
yield* fs.writeFileString(path.join(customDir, '.gitkeep'), '');
|
|
74
85
|
}), { concurrency: 'unbounded', discard: true });
|
|
75
|
-
// ── 5.
|
|
76
|
-
// Run the canonical registry script so the file is immediately present
|
|
77
|
-
// with the correct CustomRegistryEntry format. The empty callbacks/ and
|
|
78
|
-
// stages/ dirs produce empty registries — no custom components yet.
|
|
79
|
-
yield* Console.log('Generating custom component registry...');
|
|
80
|
-
yield* runRegistryScript(resolvedDir);
|
|
81
|
-
// ── 6. Write .generator-version ────────────────────────────────────────
|
|
86
|
+
// ── 5. Write .generator-version ────────────────────────────────────────
|
|
82
87
|
yield* writeVersion(resolvedDir, {
|
|
83
88
|
version: resolvedVersion,
|
|
84
89
|
generatedAt: new Date().toISOString(),
|
|
85
90
|
});
|
|
86
|
-
// ──
|
|
91
|
+
// ── 6. Print next steps ───────────────────────────────────────────────
|
|
87
92
|
yield* Console.log(nextStepsMessage(directory));
|
|
88
93
|
});
|
|
89
94
|
export const initCommand = Command.make('init', {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { Command } from '@effect/cli';
|
|
2
|
-
import { Option } from 'effect';
|
|
3
|
-
export
|
|
2
|
+
import { Effect, Option } from 'effect';
|
|
3
|
+
export interface UpdateProjectOptions {
|
|
4
|
+
readonly directory: Option.Option<string>;
|
|
5
|
+
readonly local: Option.Option<string>;
|
|
6
|
+
readonly tag: Option.Option<string>;
|
|
7
|
+
}
|
|
8
|
+
export declare const updateProject: ({ directory, local, tag }: UpdateProjectOptions) => Effect.Effect<void, 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, import("@effect/platform/FileSystem").FileSystem | import("@effect/platform/Path").Path | import("../services/release.js").Release>;
|
|
9
|
+
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, {
|
|
4
10
|
readonly directory: Option.Option<string>;
|
|
5
11
|
readonly local: Option.Option<string>;
|
|
6
12
|
readonly tag: Option.Option<string>;
|
|
@@ -2,14 +2,10 @@ import { Command, Options } from '@effect/cli';
|
|
|
2
2
|
import { Console, Effect, Option } from 'effect';
|
|
3
3
|
import nodePath from 'node:path';
|
|
4
4
|
import { assertValidProject, writeVersion } from '../config/version.js';
|
|
5
|
-
import { copyWithExclusions, expandTilde } from '../services/file-system.js';
|
|
6
|
-
import {
|
|
5
|
+
import { copyWithExclusions, copyWrapperFiles, expandTilde } from '../services/file-system.js';
|
|
6
|
+
import { getTemplatesDir } from './generate.js';
|
|
7
7
|
import { resolveSource } from './source.js';
|
|
8
|
-
export const
|
|
9
|
-
directory: Options.optional(Options.text('directory').pipe(Options.withDescription('Project root directory. Defaults to the current working directory.'))),
|
|
10
|
-
local: Options.optional(Options.text('local').pipe(Options.withDescription('Path to a local framework directory. If omitted, the main branch is downloaded from GitHub.'))),
|
|
11
|
-
tag: Options.optional(Options.text('tag').pipe(Options.withDescription('Framework Git release tag to update to (e.g. @forgerock/login-widget@2.1.0 or v1.2.0). If omitted, the main branch is used.'))),
|
|
12
|
-
}, ({ local, tag, directory }) => Effect.gen(function* () {
|
|
8
|
+
export const updateProject = ({ directory, local, tag }) => Effect.gen(function* () {
|
|
13
9
|
const cwd = nodePath.resolve(expandTilde(Option.getOrElse(directory, () => process.cwd())));
|
|
14
10
|
// ── 1. Verify this is an initialized project ──────────────────────────
|
|
15
11
|
const currentVersion = yield* assertValidProject(cwd);
|
|
@@ -21,17 +17,28 @@ export const updateCommand = Command.make('update', {
|
|
|
21
17
|
const { sourceDir, resolvedVersion } = yield* resolveSource(local, tag, cwd);
|
|
22
18
|
yield* Console.log('Copying updated framework files...');
|
|
23
19
|
yield* copyWithExclusions(sourceDir, cwd);
|
|
20
|
+
// ── 2b. Backfill missing wrapper files ─────────────────────────────
|
|
21
|
+
// The bulk copy skips them (they are file-protected so customer edits
|
|
22
|
+
// survive), but a project updated from an older framework version may
|
|
23
|
+
// not have them yet — the app's +page.svelte imports them directly, so
|
|
24
|
+
// a project missing one cannot build. copyWrapperFiles copies only
|
|
25
|
+
// absent files, from the CLI's own wrapper templates, never
|
|
26
|
+
// overwriting customer edits.
|
|
27
|
+
yield* copyWrapperFiles(cwd, getTemplatesDir());
|
|
24
28
|
return resolvedVersion;
|
|
25
29
|
}));
|
|
26
|
-
// ── 3.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// ── 4. Update .generator-version ──────────────────────────────────────
|
|
30
|
+
// ── 3. Update .generator-version ──────────────────────────────────────
|
|
31
|
+
// custom-registry.ts is NOT regenerated here — the framework's Vite
|
|
32
|
+
// plugin regenerates it on the next build or dev-server start.
|
|
30
33
|
yield* writeVersion(cwd, {
|
|
31
34
|
version: resolvedVersion,
|
|
32
35
|
generatedAt: new Date().toISOString(),
|
|
33
36
|
});
|
|
34
37
|
yield* Console.log(`\nDone. Updated from ${currentVersion.version} to ${resolvedVersion}.\n` +
|
|
35
|
-
'Custom component registry regenerated.\n' +
|
|
36
38
|
'Run "pnpm install" if package dependencies changed.\n');
|
|
37
|
-
})
|
|
39
|
+
});
|
|
40
|
+
export const updateCommand = Command.make('update', {
|
|
41
|
+
directory: Options.optional(Options.text('directory').pipe(Options.withDescription('Project root directory. Defaults to the current working directory.'))),
|
|
42
|
+
local: Options.optional(Options.text('local').pipe(Options.withDescription('Path to a local framework directory. If omitted, the main branch is downloaded from GitHub.'))),
|
|
43
|
+
tag: Options.optional(Options.text('tag').pipe(Options.withDescription('Framework Git release tag to update to (e.g. @forgerock/login-widget@2.1.0 or v1.2.0). If omitted, the main branch is used.'))),
|
|
44
|
+
}, updateProject).pipe(Command.withDescription('Fetch the latest (or a specified) framework version and overwrite core files while preserving experimental/custom/.'));
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -79,12 +79,4 @@ export declare class ComponentAlreadyExistsError extends ComponentAlreadyExistsE
|
|
|
79
79
|
readonly path: string;
|
|
80
80
|
}> {
|
|
81
81
|
}
|
|
82
|
-
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 & {
|
|
83
|
-
readonly _tag: "RegistryScanError";
|
|
84
|
-
} & Readonly<A>;
|
|
85
|
-
export declare class RegistryScanError extends RegistryScanError_base<{
|
|
86
|
-
readonly directory: string;
|
|
87
|
-
readonly cause?: unknown;
|
|
88
|
-
}> {
|
|
89
|
-
}
|
|
90
82
|
export {};
|
package/dist/src/errors.js
CHANGED
|
@@ -21,5 +21,3 @@ export class InvalidComponentNameError extends Data.TaggedError('InvalidComponen
|
|
|
21
21
|
}
|
|
22
22
|
export class ComponentAlreadyExistsError extends Data.TaggedError('ComponentAlreadyExistsError') {
|
|
23
23
|
}
|
|
24
|
-
export class RegistryScanError extends Data.TaggedError('RegistryScanError') {
|
|
25
|
-
}
|
package/dist/src/mcp.js
CHANGED
|
@@ -10,7 +10,6 @@ import { initProject } from './commands/init.js';
|
|
|
10
10
|
import { resolveSource } from './commands/source.js';
|
|
11
11
|
import { assertValidProject, writeVersion } from './config/version.js';
|
|
12
12
|
import { copyWithExclusions, expandTilde } from './services/file-system.js';
|
|
13
|
-
import { runRegistryScript } from './services/registry.js';
|
|
14
13
|
import { GithubReleaseLayer, Release } from './services/release.js';
|
|
15
14
|
// Resolve package.json from either dist/src/ (compiled) or src/ (Vitest).
|
|
16
15
|
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -38,8 +37,6 @@ function formatError(cause) {
|
|
|
38
37
|
return `Filesystem error (${e['operation']}) at "${e['path']}": ${e['cause']}`;
|
|
39
38
|
case 'GeneratorVersionError':
|
|
40
39
|
return `Generator version error: ${e['message']}${e['path'] ? ` (${e['path']})` : ''}`;
|
|
41
|
-
case 'RegistryScanError':
|
|
42
|
-
return `Registry scan failed in "${e['directory']}": ${e['cause']}`;
|
|
43
40
|
case 'DirectoryConflictError':
|
|
44
41
|
return `"${e['path']}" already contains a framework project. Use local path instead.`;
|
|
45
42
|
case 'DirectoryNotEmptyError':
|
|
@@ -158,7 +155,6 @@ const handlerLayer = mcpToolkit.toLayer({
|
|
|
158
155
|
yield* copyWithExclusions(sourceDir, cwd);
|
|
159
156
|
return resolvedVersion;
|
|
160
157
|
}));
|
|
161
|
-
yield* runRegistryScript(cwd);
|
|
162
158
|
yield* writeVersion(cwd, {
|
|
163
159
|
version: resolvedVersion,
|
|
164
160
|
generatedAt: new Date().toISOString(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FileSystem } from '@effect/platform';
|
|
1
|
+
import { FileSystem, Path } from '@effect/platform';
|
|
2
2
|
import { Effect } from 'effect';
|
|
3
3
|
import { FileSystemError } from '../errors.js';
|
|
4
4
|
/**
|
|
@@ -8,6 +8,37 @@ import { FileSystemError } from '../errors.js';
|
|
|
8
8
|
* that the directory was created by `ping-lf init`.
|
|
9
9
|
*/
|
|
10
10
|
export declare const isFrameworkDirectory: (dir: string) => Effect.Effect<boolean, never, FileSystem.FileSystem>;
|
|
11
|
+
/**
|
|
12
|
+
* Framework-shipped custom wrapper files. Each is a fixed, user-editable file
|
|
13
|
+
* rendered by the login app (`+page.svelte` imports it directly). The bulk
|
|
14
|
+
* copy skips them so `update` can never overwrite customer edits;
|
|
15
|
+
* `copyWrapperFiles` backfills any the target is missing from the CLI's own
|
|
16
|
+
* wrapper templates (see WRAPPER_TEMPLATE_ELEMENTS) so a project updated onto
|
|
17
|
+
* a framework version whose app imports the wrappers still gets the defaults.
|
|
18
|
+
*/
|
|
19
|
+
export declare const CUSTOM_WRAPPER_FILES: readonly ["experimental/custom/header/header.svelte", "experimental/custom/footer/footer.svelte"];
|
|
20
|
+
/**
|
|
21
|
+
* The page elements that ship a wrapper file. `copyWrapperFiles` copies
|
|
22
|
+
* `templates/wrapper/<element>.svelte` (the CLI's own shipped templates,
|
|
23
|
+
* resolved like the callback/stage scaffold templates) into
|
|
24
|
+
* `experimental/custom/<element>/<element>.svelte`.
|
|
25
|
+
*
|
|
26
|
+
* The CLI never reads the framework's `core/` tree — build-time vs. runtime
|
|
27
|
+
* boundary — so it owns copies of the wrapper templates instead of importing
|
|
28
|
+
* the framework's `core/.../wrapper-templates/`. The per-element order
|
|
29
|
+
* matches CUSTOM_WRAPPER_FILES.
|
|
30
|
+
*/
|
|
31
|
+
export declare const WRAPPER_TEMPLATE_ELEMENTS: readonly ["header", "footer"];
|
|
11
32
|
/** Expand a leading `~` to the user's home directory. */
|
|
12
33
|
export declare function expandTilde(p: string): string;
|
|
13
34
|
export declare const copyWithExclusions: (sourceDir: string, targetDir: string) => Effect.Effect<void, FileSystemError, FileSystem.FileSystem>;
|
|
35
|
+
/**
|
|
36
|
+
* Backfills the framework-shipped wrapper files into `targetDir`, skipping any
|
|
37
|
+
* the target already has. Customer edits are never overwritten — a file is
|
|
38
|
+
* copied only when absent. The copy reads from the CLI's own wrapper templates
|
|
39
|
+
* (`templates/wrapper/`, passed as `templatesDir`), mirroring the
|
|
40
|
+
* callback/stage scaffold templates. A missing template is a no-op, which
|
|
41
|
+
* keeps older CLI versions (whose paired framework app does not import
|
|
42
|
+
* wrappers) safe.
|
|
43
|
+
*/
|
|
44
|
+
export declare const copyWrapperFiles: (targetDir: string, templatesDir: string) => Effect.Effect<void, FileSystemError, FileSystem.FileSystem | Path.Path>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FileSystem } from '@effect/platform';
|
|
1
|
+
import { FileSystem, Path } from '@effect/platform';
|
|
2
2
|
import { Effect } from 'effect';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { isExcluded } from '../config/exclusions.js';
|
|
@@ -23,6 +23,30 @@ export const isFrameworkDirectory = Effect.fnUntraced(function* (dir) {
|
|
|
23
23
|
return hasPkgJson && (hasExperimental || hasPnpmWorkspace);
|
|
24
24
|
});
|
|
25
25
|
const PROTECTED_DIRS = ['experimental/custom/callbacks/', 'experimental/custom/stages/'];
|
|
26
|
+
/**
|
|
27
|
+
* Framework-shipped custom wrapper files. Each is a fixed, user-editable file
|
|
28
|
+
* rendered by the login app (`+page.svelte` imports it directly). The bulk
|
|
29
|
+
* copy skips them so `update` can never overwrite customer edits;
|
|
30
|
+
* `copyWrapperFiles` backfills any the target is missing from the CLI's own
|
|
31
|
+
* wrapper templates (see WRAPPER_TEMPLATE_ELEMENTS) so a project updated onto
|
|
32
|
+
* a framework version whose app imports the wrappers still gets the defaults.
|
|
33
|
+
*/
|
|
34
|
+
export const CUSTOM_WRAPPER_FILES = [
|
|
35
|
+
'experimental/custom/header/header.svelte',
|
|
36
|
+
'experimental/custom/footer/footer.svelte',
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* The page elements that ship a wrapper file. `copyWrapperFiles` copies
|
|
40
|
+
* `templates/wrapper/<element>.svelte` (the CLI's own shipped templates,
|
|
41
|
+
* resolved like the callback/stage scaffold templates) into
|
|
42
|
+
* `experimental/custom/<element>/<element>.svelte`.
|
|
43
|
+
*
|
|
44
|
+
* The CLI never reads the framework's `core/` tree — build-time vs. runtime
|
|
45
|
+
* boundary — so it owns copies of the wrapper templates instead of importing
|
|
46
|
+
* the framework's `core/.../wrapper-templates/`. The per-element order
|
|
47
|
+
* matches CUSTOM_WRAPPER_FILES.
|
|
48
|
+
*/
|
|
49
|
+
export const WRAPPER_TEMPLATE_ELEMENTS = ['header', 'footer'];
|
|
26
50
|
/** Expand a leading `~` to the user's home directory. */
|
|
27
51
|
export function expandTilde(p) {
|
|
28
52
|
if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) {
|
|
@@ -33,7 +57,8 @@ export function expandTilde(p) {
|
|
|
33
57
|
}
|
|
34
58
|
function isProtected(relativePath) {
|
|
35
59
|
const normalized = normalizeSeparators(relativePath);
|
|
36
|
-
return PROTECTED_DIRS.some((dir) => `${normalized}/`.startsWith(dir))
|
|
60
|
+
return (PROTECTED_DIRS.some((dir) => `${normalized}/`.startsWith(dir)) ||
|
|
61
|
+
CUSTOM_WRAPPER_FILES.includes(normalized));
|
|
37
62
|
}
|
|
38
63
|
const walk = (fs, sourceDir, targetDir, dir) => Effect.gen(function* () {
|
|
39
64
|
const entries = yield* fs.readDirectory(dir).pipe(wrapFsCall('readDirectory', dir));
|
|
@@ -62,3 +87,40 @@ export const copyWithExclusions = (sourceDir, targetDir) => Effect.gen(function*
|
|
|
62
87
|
const fs = yield* FileSystem.FileSystem;
|
|
63
88
|
yield* walk(fs, sourceDir, targetDir, sourceDir);
|
|
64
89
|
});
|
|
90
|
+
/**
|
|
91
|
+
* Backfills the framework-shipped wrapper files into `targetDir`, skipping any
|
|
92
|
+
* the target already has. Customer edits are never overwritten — a file is
|
|
93
|
+
* copied only when absent. The copy reads from the CLI's own wrapper templates
|
|
94
|
+
* (`templates/wrapper/`, passed as `templatesDir`), mirroring the
|
|
95
|
+
* callback/stage scaffold templates. A missing template is a no-op, which
|
|
96
|
+
* keeps older CLI versions (whose paired framework app does not import
|
|
97
|
+
* wrappers) safe.
|
|
98
|
+
*/
|
|
99
|
+
export const copyWrapperFiles = (targetDir, templatesDir) => Effect.gen(function* () {
|
|
100
|
+
const fs = yield* FileSystem.FileSystem;
|
|
101
|
+
const path = yield* Path.Path;
|
|
102
|
+
yield* Effect.forEach(
|
|
103
|
+
// Pair each wrapper target element with its template source. Positional
|
|
104
|
+
// zip on two pinned constants: the contract test pins both arrays and
|
|
105
|
+
// their per-element order, so an accidental desync fails the build.
|
|
106
|
+
WRAPPER_TEMPLATE_ELEMENTS.map((element, index) => ({
|
|
107
|
+
element,
|
|
108
|
+
wrapperPath: CUSTOM_WRAPPER_FILES[index],
|
|
109
|
+
})), ({ element, wrapperPath }) => Effect.gen(function* () {
|
|
110
|
+
const sourcePath = path.join(templatesDir, 'wrapper', `${element}.svelte`);
|
|
111
|
+
const targetPath = path.join(targetDir, wrapperPath);
|
|
112
|
+
const hasSource = yield* fs.exists(sourcePath).pipe(Effect.orElseSucceed(() => false));
|
|
113
|
+
if (!hasSource) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const hasTarget = yield* fs.exists(targetPath).pipe(Effect.orElseSucceed(() => false));
|
|
117
|
+
if (hasTarget) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const parentDir = path.dirname(targetPath);
|
|
121
|
+
yield* fs
|
|
122
|
+
.makeDirectory(parentDir, { recursive: true })
|
|
123
|
+
.pipe(wrapFsCall('makeDirectory', parentDir));
|
|
124
|
+
yield* fs.copyFile(sourcePath, targetPath).pipe(wrapFsCall('copyFile', sourcePath));
|
|
125
|
+
}), { concurrency: 'unbounded', discard: true });
|
|
126
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Custom page footer wrapper.
|
|
3
|
+
─────────────────────────────
|
|
4
|
+
This is your file: the framework renders it below the journey container
|
|
5
|
+
(inside the root div of the login app's +page.svelte). It is never
|
|
6
|
+
overwritten once it exists — not by rebuilds, not by `ping-lf update` — so
|
|
7
|
+
edit it freely; your changes survive framework upgrades. If you ever delete
|
|
8
|
+
it, the next build or dev-server start regenerates the default (only when
|
|
9
|
+
the file is absent; your edits are never touched).
|
|
10
|
+
|
|
11
|
+
HOW IT RENDERS
|
|
12
|
+
──────────────
|
|
13
|
+
The footer mounts client-side only: it first renders once a journey step
|
|
14
|
+
arrives, then stays mounted for the rest of the session (no flicker while
|
|
15
|
+
steps load, switch journeys, or redirect on completion).
|
|
16
|
+
|
|
17
|
+
PROPS
|
|
18
|
+
─────
|
|
19
|
+
The wrapper receives exactly one prop:
|
|
20
|
+
|
|
21
|
+
journeyName: string
|
|
22
|
+
Name of the journey currently driving the page (e.g. "Login",
|
|
23
|
+
"Register"). `''` means the AM default journey — the one the tenant
|
|
24
|
+
serves when the URL names no journey. Show your default-journey view
|
|
25
|
+
in that case; the footer always renders something.
|
|
26
|
+
|
|
27
|
+
The framework passes no other props. Do not add required props of your own —
|
|
28
|
+
they will arrive undefined. Anything you need beyond `journeyName`, derive
|
|
29
|
+
inside this component from the framework exports.
|
|
30
|
+
-->
|
|
31
|
+
|
|
32
|
+
<script lang="ts">
|
|
33
|
+
// ─── Framework imports ──────────────────────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* Import everything you need from '$login-framework' — the
|
|
36
|
+
* framework's centralized exports for custom components. No need to reach
|
|
37
|
+
* into internal aliases like $core, $components, or $journey directly.
|
|
38
|
+
*
|
|
39
|
+
* Available exports (see experimental/custom/login-framework.ts for the full list).
|
|
40
|
+
*/
|
|
41
|
+
import type { CustomWrapperProps } from '$login-framework';
|
|
42
|
+
|
|
43
|
+
/** The one prop the framework passes: the active journey name (`''` = default). */
|
|
44
|
+
export let journeyName: CustomWrapperProps['journeyName'] = '';
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<!--
|
|
48
|
+
Default markup: neutral no-op placeholder. Replace it with your own branding.
|
|
49
|
+
|
|
50
|
+
Example — branch the markup on the prop value. Render the copyright line
|
|
51
|
+
plus "Create your account" when journeyName is "Register", the plain
|
|
52
|
+
copyright line when it is the empty string (that is the AM default journey,
|
|
53
|
+
the case where the URL named no journey), and a journeyName-suffixed
|
|
54
|
+
copyright line otherwise. In Svelte that is an if/else if/else chain on
|
|
55
|
+
journeyName.
|
|
56
|
+
|
|
57
|
+
All Tailwind classes use the framework's tw_ prefix so theme compilation
|
|
58
|
+
picks them up (see themes/default/config.cjs — prefix: 'tw_').
|
|
59
|
+
-->
|
|
60
|
+
<footer class="tw_hidden"></footer>
|
|
61
|
+
|
|
62
|
+
<!--
|
|
63
|
+
Scoped styles for this wrapper. They apply only here — they won't leak to the
|
|
64
|
+
rest of the widget.
|
|
65
|
+
-->
|
|
66
|
+
<style>
|
|
67
|
+
/* Add your custom footer styles here. */
|
|
68
|
+
</style>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Custom page header wrapper.
|
|
3
|
+
─────────────────────────────
|
|
4
|
+
This is your file: the framework renders it above the journey container
|
|
5
|
+
(inside the root div of the login app's +page.svelte). It is never
|
|
6
|
+
overwritten once it exists — not by rebuilds, not by `ping-lf update` — so
|
|
7
|
+
edit it freely; your changes survive framework upgrades. If you ever delete
|
|
8
|
+
it, the next build or dev-server start regenerates the default (only when
|
|
9
|
+
the file is absent; your edits are never touched).
|
|
10
|
+
|
|
11
|
+
HOW IT RENDERS
|
|
12
|
+
──────────────
|
|
13
|
+
The header mounts client-side only: it first renders once a journey step
|
|
14
|
+
arrives, then stays mounted for the rest of the session (no flicker while
|
|
15
|
+
steps load, switch journeys, or redirect on completion).
|
|
16
|
+
|
|
17
|
+
PROPS
|
|
18
|
+
─────
|
|
19
|
+
The wrapper receives exactly one prop:
|
|
20
|
+
|
|
21
|
+
journeyName: string
|
|
22
|
+
Name of the journey currently driving the page (e.g. "Login",
|
|
23
|
+
"Register"). `''` means the AM default journey — the one the tenant
|
|
24
|
+
serves when the URL names no journey. Show your default-journey view
|
|
25
|
+
in that case; the header always renders something.
|
|
26
|
+
|
|
27
|
+
The framework passes no other props. Do not add required props of your own —
|
|
28
|
+
they will arrive undefined. Anything you need beyond `journeyName`, derive
|
|
29
|
+
inside this component from the framework exports.
|
|
30
|
+
-->
|
|
31
|
+
|
|
32
|
+
<script lang="ts">
|
|
33
|
+
// ─── Framework imports ──────────────────────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* Import everything you need from '$login-framework' — the
|
|
36
|
+
* framework's centralized exports for custom components. No need to reach
|
|
37
|
+
* into internal aliases like $core, $components, or $journey directly.
|
|
38
|
+
*
|
|
39
|
+
* Available exports (see experimental/custom/login-framework.ts for the full list).
|
|
40
|
+
*/
|
|
41
|
+
import type { CustomWrapperProps } from '$login-framework';
|
|
42
|
+
|
|
43
|
+
/** The one prop the framework passes: the active journey name (`''` = default). */
|
|
44
|
+
export let journeyName: CustomWrapperProps['journeyName'] = '';
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<!--
|
|
48
|
+
Default markup: neutral no-op placeholder. Replace it with your own branding.
|
|
49
|
+
|
|
50
|
+
Example — branch the markup on the prop value. Render a "Register" banner
|
|
51
|
+
when journeyName is "Register", a default-journey banner when it is the
|
|
52
|
+
empty string (that is the AM default journey, the case where the URL named
|
|
53
|
+
no journey), and a journeyName-driven banner otherwise. In Svelte that is
|
|
54
|
+
an if/else if/else chain on journeyName.
|
|
55
|
+
|
|
56
|
+
All Tailwind classes use the framework's tw_ prefix so theme compilation
|
|
57
|
+
picks them up (see themes/default/config.cjs — prefix: 'tw_').
|
|
58
|
+
-->
|
|
59
|
+
<header class="tw_hidden"></header>
|
|
60
|
+
|
|
61
|
+
<!--
|
|
62
|
+
Scoped styles for this wrapper. They apply only here — they won't leak to the
|
|
63
|
+
rest of the widget.
|
|
64
|
+
-->
|
|
65
|
+
<style>
|
|
66
|
+
/* Add your custom header styles here. */
|
|
67
|
+
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgerock/login-framework-cli",
|
|
3
|
-
"version": "0.0.0-beta-
|
|
3
|
+
"version": "0.0.0-beta-20261021173425",
|
|
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": {
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"@effect/printer-ansi": "^0.49.0",
|
|
45
45
|
"@effect/typeclass": "^0.40.0",
|
|
46
46
|
"effect": "^3.21.0",
|
|
47
|
-
"svelte": "^5.55.7",
|
|
48
47
|
"tar": "^7.5.21"
|
|
49
48
|
},
|
|
50
49
|
"devDependencies": {
|
|
50
|
+
"@effect/vitest": "0.29.0",
|
|
51
51
|
"typescript": "^5.2.2",
|
|
52
52
|
"vitest": "^3.2.6"
|
|
53
53
|
},
|
|
@@ -1,29 +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
|
-
interface ComponentEntry {
|
|
6
|
-
filePath: string;
|
|
7
|
-
name: string;
|
|
8
|
-
type: ComponentType;
|
|
9
|
-
acceptedProps: string[];
|
|
10
|
-
}
|
|
11
|
-
/** Extracts names of all `export let` prop declarations from a Svelte component's instance script. */
|
|
12
|
-
export declare function parseAcceptedProps(content: string): string[];
|
|
13
|
-
/** Parses and validates the leading `<!-- @component -->` block from a Svelte file. */
|
|
14
|
-
export declare const parseComponentHeader: (filePath: string, content: string) => Effect.Effect<{
|
|
15
|
-
type: ComponentType;
|
|
16
|
-
name: string;
|
|
17
|
-
}, RegistryScanError>;
|
|
18
|
-
export declare function buildRegistryContent(path: Path.Path, registryDir: string, stageComponents: ComponentEntry[], callbackComponents: ComponentEntry[]): string;
|
|
19
|
-
/**
|
|
20
|
-
* Scans `experimental/custom/stages/` and `experimental/custom/callbacks/` for
|
|
21
|
-
* `@component`-annotated Svelte files and writes
|
|
22
|
-
* `core/journey/_utilities/custom-registry.ts`.
|
|
23
|
-
*
|
|
24
|
-
* All I/O runs in-process via the platform `FileSystem` service — no subprocess
|
|
25
|
-
* spawning. Validation errors across multiple components are collected and
|
|
26
|
-
* reported together.
|
|
27
|
-
*/
|
|
28
|
-
export declare const runRegistryScript: (projectDir: string) => Effect.Effect<void, RegistryScanError, FileSystem.FileSystem | Path.Path>;
|
|
29
|
-
export {};
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { FileSystem, Path } from '@effect/platform';
|
|
2
|
-
import { Console, Effect } from 'effect';
|
|
3
|
-
import { parse } from 'svelte/compiler';
|
|
4
|
-
import { RegistryScanError } from '../errors.js';
|
|
5
|
-
// --------------------------------------------------------------------------
|
|
6
|
-
// Helpers (exported for testing)
|
|
7
|
-
// --------------------------------------------------------------------------
|
|
8
|
-
/** Extracts names of all `export let` prop declarations from a Svelte component's instance script. */
|
|
9
|
-
export function parseAcceptedProps(content) {
|
|
10
|
-
const abstractSyntaxTree = parse(content, { modern: false });
|
|
11
|
-
const props = [];
|
|
12
|
-
for (const node of abstractSyntaxTree.instance?.content.body ?? []) {
|
|
13
|
-
if (node.type !== 'ExportNamedDeclaration') {
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
if (node.declaration?.type === 'VariableDeclaration' && node.declaration.kind === 'let') {
|
|
17
|
-
for (const declarator of node.declaration.declarations) {
|
|
18
|
-
if (declarator.id.type === 'Identifier') {
|
|
19
|
-
props.push(declarator.id.name);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return props;
|
|
25
|
-
}
|
|
26
|
-
function toPascalCase(str) {
|
|
27
|
-
return str
|
|
28
|
-
.replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase())
|
|
29
|
-
.replace(/^(.)/, (_, chr) => chr.toUpperCase());
|
|
30
|
-
}
|
|
31
|
-
/** Parses and validates the leading `<!-- @component -->` block from a Svelte file. */
|
|
32
|
-
export const parseComponentHeader = (filePath, content) => {
|
|
33
|
-
const fail = (cause) => Effect.fail(new RegistryScanError({ directory: filePath, cause }));
|
|
34
|
-
const commentMatch = content.match(/^<!--([\s\S]*?)-->/);
|
|
35
|
-
if (!commentMatch)
|
|
36
|
-
return fail('Missing @component header. Every custom component must begin with:\n' +
|
|
37
|
-
'<!--\n @component\n Type: stage|callback\n Name: <ComponentName>\n -->');
|
|
38
|
-
const block = commentMatch[1];
|
|
39
|
-
if (!block.includes('@component'))
|
|
40
|
-
return fail('Missing "@component" tag in the opening comment.');
|
|
41
|
-
const typeMatch = block.match(/Type:\s*(\S+)/);
|
|
42
|
-
if (!typeMatch)
|
|
43
|
-
return fail('Missing "Type:" field in @component header. Expected: Type: stage or Type: callback');
|
|
44
|
-
const rawType = typeMatch[1].toLowerCase();
|
|
45
|
-
if (rawType !== 'stage' && rawType !== 'callback')
|
|
46
|
-
return fail(`Invalid Type value "${typeMatch[1]}". Must be "stage" or "callback".`);
|
|
47
|
-
const nameMatch = block.match(/Name:\s*(.+)/);
|
|
48
|
-
if (!nameMatch)
|
|
49
|
-
return fail('Missing "Name:" field in @component header. Expected: Name: <ComponentName>');
|
|
50
|
-
return Effect.succeed({ type: rawType, name: nameMatch[1].trim() });
|
|
51
|
-
};
|
|
52
|
-
// --------------------------------------------------------------------------
|
|
53
|
-
// File scanning
|
|
54
|
-
// --------------------------------------------------------------------------
|
|
55
|
-
const findSvelteFiles = (fs, path, dir) => fs.exists(dir).pipe(Effect.orElseSucceed(() => false), Effect.flatMap((exists) => !exists
|
|
56
|
-
? Effect.succeed([])
|
|
57
|
-
: fs.readDirectory(dir).pipe(Effect.mapError((cause) => new RegistryScanError({ directory: dir, cause })), Effect.flatMap((entries) => Effect.forEach(entries, (entry) => {
|
|
58
|
-
const fullPath = path.join(dir, entry);
|
|
59
|
-
return fs.stat(fullPath).pipe(Effect.mapError((cause) => new RegistryScanError({ directory: dir, cause })), Effect.flatMap((stat) => stat.type === 'Directory'
|
|
60
|
-
? findSvelteFiles(fs, path, fullPath)
|
|
61
|
-
: Effect.succeed(entry.endsWith('.svelte') && !entry.endsWith('.story.svelte')
|
|
62
|
-
? [fullPath]
|
|
63
|
-
: [])));
|
|
64
|
-
}, { concurrency: 'unbounded' })), Effect.map((nested) => nested.flat()))));
|
|
65
|
-
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
|
|
66
|
-
? Effect.fail(new RegistryScanError({
|
|
67
|
-
directory: filePath,
|
|
68
|
-
cause: `Component in /experimental/custom/${expectedType}s/ declares Type: "${type}". Must declare Type: ${expectedType}`,
|
|
69
|
-
}))
|
|
70
|
-
: Effect.succeed({
|
|
71
|
-
filePath,
|
|
72
|
-
name,
|
|
73
|
-
type,
|
|
74
|
-
acceptedProps: parseAcceptedProps(content),
|
|
75
|
-
})))))).pipe(Effect.mapError((errors) => new RegistryScanError({
|
|
76
|
-
directory: dir,
|
|
77
|
-
cause: errors.map((e) => String(e.cause)).join('\n'),
|
|
78
|
-
})))));
|
|
79
|
-
// --------------------------------------------------------------------------
|
|
80
|
-
// Registry content builder (pure)
|
|
81
|
-
// --------------------------------------------------------------------------
|
|
82
|
-
export function buildRegistryContent(path, registryDir, stageComponents, callbackComponents) {
|
|
83
|
-
const toEntry = (prefix) => ({ filePath, name, acceptedProps }) => {
|
|
84
|
-
const relPath = path.relative(registryDir, filePath).replace(/\\/g, '/');
|
|
85
|
-
const importPath = relPath.startsWith('.') ? relPath : `./${relPath}`;
|
|
86
|
-
return { varName: `${prefix}${toPascalCase(name)}`, importPath, name, acceptedProps };
|
|
87
|
-
};
|
|
88
|
-
const stageEntries = stageComponents.map(toEntry('Stage'));
|
|
89
|
-
const callbackEntries = callbackComponents.map(toEntry('Callback'));
|
|
90
|
-
const lines = [
|
|
91
|
-
`/**`,
|
|
92
|
-
` * AUTO-GENERATED — do not edit by hand.`,
|
|
93
|
-
` * Run \`pnpm build:widget\` or \`pnpm --filter @forgerock/login-widget exec vite build\` to regenerate.`,
|
|
94
|
-
` *`,
|
|
95
|
-
` * Source: /experimental/custom/stages/ and /experimental/custom/callbacks/`,
|
|
96
|
-
` */`,
|
|
97
|
-
``,
|
|
98
|
-
`import type { Component } from 'svelte';`,
|
|
99
|
-
``,
|
|
100
|
-
`export interface CustomRegistryEntry {`,
|
|
101
|
-
` component: Component;`,
|
|
102
|
-
` /** Props declared via \`export let\` in the component — only these are forwarded by the mapper. */`,
|
|
103
|
-
` acceptedProps: string[];`,
|
|
104
|
-
`}`,
|
|
105
|
-
``,
|
|
106
|
-
];
|
|
107
|
-
if (stageEntries.length > 0) {
|
|
108
|
-
lines.push(`// Stage overrides / extensions`);
|
|
109
|
-
for (const { varName, importPath } of stageEntries)
|
|
110
|
-
lines.push(`import ${varName} from '${importPath}';`);
|
|
111
|
-
lines.push(``);
|
|
112
|
-
}
|
|
113
|
-
if (callbackEntries.length > 0) {
|
|
114
|
-
lines.push(`// Callback overrides / extensions`);
|
|
115
|
-
for (const { varName, importPath } of callbackEntries)
|
|
116
|
-
lines.push(`import ${varName} from '${importPath}';`);
|
|
117
|
-
lines.push(``);
|
|
118
|
-
}
|
|
119
|
-
lines.push(`export const customStageRegistry: Record<string, CustomRegistryEntry> = {`);
|
|
120
|
-
for (const { varName, name, acceptedProps } of stageEntries)
|
|
121
|
-
lines.push(` ${JSON.stringify(name)}: { get component() { return ${varName}; }, acceptedProps: ${JSON.stringify(acceptedProps)} },`);
|
|
122
|
-
lines.push(`};`);
|
|
123
|
-
lines.push(``);
|
|
124
|
-
lines.push(`export const customCallbackRegistry: Record<string, CustomRegistryEntry> = {`);
|
|
125
|
-
for (const { varName, name, acceptedProps } of callbackEntries)
|
|
126
|
-
lines.push(` ${JSON.stringify(name)}: { get component() { return ${varName}; }, acceptedProps: ${JSON.stringify(acceptedProps)} },`);
|
|
127
|
-
lines.push(`};`);
|
|
128
|
-
lines.push(``);
|
|
129
|
-
return lines.join('\n');
|
|
130
|
-
}
|
|
131
|
-
// --------------------------------------------------------------------------
|
|
132
|
-
// Public API
|
|
133
|
-
// --------------------------------------------------------------------------
|
|
134
|
-
/**
|
|
135
|
-
* Scans `experimental/custom/stages/` and `experimental/custom/callbacks/` for
|
|
136
|
-
* `@component`-annotated Svelte files and writes
|
|
137
|
-
* `core/journey/_utilities/custom-registry.ts`.
|
|
138
|
-
*
|
|
139
|
-
* All I/O runs in-process via the platform `FileSystem` service — no subprocess
|
|
140
|
-
* spawning. Validation errors across multiple components are collected and
|
|
141
|
-
* reported together.
|
|
142
|
-
*/
|
|
143
|
-
export const runRegistryScript = (projectDir) => Effect.gen(function* () {
|
|
144
|
-
const fs = yield* FileSystem.FileSystem;
|
|
145
|
-
const path = yield* Path.Path;
|
|
146
|
-
const stageDir = path.join(projectDir, 'experimental', 'custom', 'stages');
|
|
147
|
-
const callbackDir = path.join(projectDir, 'experimental', 'custom', 'callbacks');
|
|
148
|
-
const registryDir = path.join(projectDir, 'core', 'journey', '_utilities', 'registry');
|
|
149
|
-
const registryPath = path.join(registryDir, 'custom-registry.ts');
|
|
150
|
-
const [stageComponents, callbackComponents] = yield* Effect.all([
|
|
151
|
-
scanDirectory(fs, path, stageDir, 'stage'),
|
|
152
|
-
scanDirectory(fs, path, callbackDir, 'callback'),
|
|
153
|
-
], { concurrency: 'unbounded' });
|
|
154
|
-
const content = buildRegistryContent(path, registryDir, stageComponents, callbackComponents);
|
|
155
|
-
yield* fs
|
|
156
|
-
.makeDirectory(registryDir, { recursive: true })
|
|
157
|
-
.pipe(Effect.mapError((cause) => new RegistryScanError({ directory: registryDir, cause })));
|
|
158
|
-
yield* fs
|
|
159
|
-
.writeFileString(registryPath, content)
|
|
160
|
-
.pipe(Effect.mapError((cause) => new RegistryScanError({ directory: registryPath, cause })));
|
|
161
|
-
const total = stageComponents.length + callbackComponents.length;
|
|
162
|
-
if (total === 0) {
|
|
163
|
-
yield* Console.log(`custom-registry.ts generated (no custom components found — registries are empty)`);
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
yield* Console.log(`custom-registry.ts generated:`);
|
|
167
|
-
if (stageComponents.length > 0)
|
|
168
|
-
yield* Console.log(` Stages (${stageComponents.length}): ${stageComponents
|
|
169
|
-
.map((c) => c.name)
|
|
170
|
-
.join(', ')}`);
|
|
171
|
-
if (callbackComponents.length > 0)
|
|
172
|
-
yield* Console.log(` Callbacks (${callbackComponents.length}): ${callbackComponents
|
|
173
|
-
.map((c) => c.name)
|
|
174
|
-
.join(', ')}`);
|
|
175
|
-
}
|
|
176
|
-
});
|