@remit/web-client 0.0.261 → 0.0.263

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/web-client",
3
- "version": "0.0.261",
3
+ "version": "0.0.263",
4
4
  "type": "module",
5
5
  "description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
6
6
  "exports": {
@@ -1,8 +1,10 @@
1
1
  import assert from "node:assert/strict";
2
- import { dirname, join } from "node:path";
3
2
  import { describe, it } from "node:test";
4
- import { fileURLToPath } from "node:url";
5
- import { build } from "esbuild";
3
+ import {
4
+ type AppGraph,
5
+ AUTH_COMPOSITIONS,
6
+ bundleAppGraph,
7
+ } from "@/test-support/test-bundle-app";
6
8
 
7
9
  /**
8
10
  * Proves the auth shell is a composition choice, not a runtime toggle inside
@@ -11,89 +13,10 @@ import { build } from "esbuild";
11
13
  * app shell and screens are walked from the real entry each time, so this fails
12
14
  * the moment any surface reaches back to a specific identity SDK.
13
15
  */
14
- const here = dirname(fileURLToPath(import.meta.url));
15
- const srcDir = join(here, "..");
16
- const packageDir = join(srcDir, "..");
17
-
18
- interface Variant {
19
- specifier: string;
20
- name: string;
21
- }
22
-
23
- const VARIANTS: Record<"betterAuth" | "cognito", Variant> = {
24
- betterAuth: {
25
- specifier: "@/auth/better-auth-provider",
26
- name: "betterAuthProvider",
27
- },
28
- cognito: {
29
- specifier: "@/auth/cognito-provider",
30
- name: "cognitoAuthProvider",
31
- },
32
- };
33
-
34
- interface Graph {
35
- inputs: string[];
36
- importPaths: string[];
37
- }
38
-
39
- // Externalize third-party packages, but bundle in-repo `@remit/*` workspace
40
- // source so the walk matches what the real vite build includes. A shared
41
- // primitive (e.g. `@remit/ui`) that pulled in Amplify would then surface in this
42
- // graph and fail the test, rather than hiding behind an external edge.
43
- const externalizeThirdParty = {
44
- name: "externalize-third-party",
45
- setup(build: import("esbuild").PluginBuild) {
46
- build.onResolve({ filter: /.*/ }, (args) => {
47
- const path = args.path;
48
- if (path.startsWith(".") || path.startsWith("/")) return null;
49
- if (path === "@" || path.startsWith("@/")) return null;
50
- if (path.startsWith("@remit/")) return null;
51
- return { path, external: true };
52
- });
53
- },
54
- };
55
-
56
- const bundleGraph = async (variant: Variant): Promise<Graph> => {
57
- const entry = [
58
- 'import { mountApp } from "@/shell";',
59
- `import { ${variant.name} } from "${variant.specifier}";`,
60
- `mountApp({ authProvider: ${variant.name} });`,
61
- "",
62
- ].join("\n");
63
-
64
- const result = await build({
65
- stdin: {
66
- contents: entry,
67
- resolveDir: packageDir,
68
- loader: "tsx",
69
- sourcefile: "compose-entry.tsx",
70
- },
71
- bundle: true,
72
- write: false,
73
- metafile: true,
74
- logLevel: "silent",
75
- format: "esm",
76
- platform: "browser",
77
- jsx: "automatic",
78
- jsxImportSource: "react",
79
- plugins: [externalizeThirdParty],
80
- loader: { ".css": "empty", ".png": "empty", ".svg": "empty" },
81
- absWorkingDir: packageDir,
82
- alias: { "@": srcDir },
83
- });
84
-
85
- const inputs = Object.keys(result.metafile.inputs);
86
- const importPaths = new Set<string>();
87
- for (const input of Object.values(result.metafile.inputs)) {
88
- for (const imported of input.imports) importPaths.add(imported.path);
89
- }
90
- return { inputs, importPaths: [...importPaths] };
91
- };
92
-
93
- const mentionsAmplify = ({ inputs, importPaths }: Graph): boolean =>
16
+ const mentionsAmplify = ({ inputs, importPaths }: AppGraph): boolean =>
94
17
  [...inputs, ...importPaths].some((path) => /aws-amplify/.test(path));
95
18
 
96
- const composesCognitoShell = ({ inputs }: Graph): boolean =>
19
+ const composesCognitoShell = ({ inputs }: AppGraph): boolean =>
97
20
  inputs.some(
98
21
  (path) =>
99
22
  path.includes("auth/cognito/") || path.includes("cognito-provider"),
@@ -101,7 +24,7 @@ const composesCognitoShell = ({ inputs }: Graph): boolean =>
101
24
 
102
25
  describe("web-client composition", () => {
103
26
  it("omits every Amplify/Cognito module when composing the better-auth provider", async () => {
104
- const graph = await bundleGraph(VARIANTS.betterAuth);
27
+ const graph = await bundleAppGraph(AUTH_COMPOSITIONS.betterAuth);
105
28
 
106
29
  assert.ok(
107
30
  graph.inputs.some((path) => path.includes("shell/index")),
@@ -120,7 +43,7 @@ describe("web-client composition", () => {
120
43
  });
121
44
 
122
45
  it("includes the Amplify/Cognito modules when composing the cognito provider", async () => {
123
- const graph = await bundleGraph(VARIANTS.cognito);
46
+ const graph = await bundleAppGraph(AUTH_COMPOSITIONS.cognito);
124
47
 
125
48
  assert.equal(
126
49
  mentionsAmplify(graph),
@@ -12,6 +12,7 @@ import {
12
12
  type MatchMode,
13
13
  type RuleClause,
14
14
  type RunState,
15
+ ruleRestrictionFor,
15
16
  type SearchConversion,
16
17
  type SelectionRestriction,
17
18
  SelectionWizard,
@@ -513,11 +514,15 @@ function SelectionWizardSession({
513
514
  // preview behind a widened door has something to count against. The one-off
514
515
  // scope and the ticked rows act on the messages themselves and are unaffected.
515
516
  const wizardScope = wizardScopeFor(accountId, selectionRestriction);
517
+ // An escalated match is restricted on the same step for its own reason: the
518
+ // list resolved it before the wizard opened, and no door on the match step can
519
+ // give it a predicate to keep matching on (#1193).
520
+ const ruleRestriction = ruleRestrictionFor(mode, wizardScope);
516
521
  const restrictionFor = (step: StepId): string | undefined => {
517
522
  if (step === "folder") return wizardScope.destination;
518
523
  if (step !== "rule") return undefined;
519
524
  return named.scope === "standing" || named.scope === "until"
520
- ? wizardScope.rule
525
+ ? ruleRestriction
521
526
  : undefined;
522
527
  };
523
528
  const blockedReason =
@@ -1048,7 +1053,7 @@ function SelectionWizardSession({
1048
1053
  draft: named,
1049
1054
  onScopeChange: (scope) => setDraft((held) => ({ ...held, scope })),
1050
1055
  onUntilChange: (until) => setDraft((held) => ({ ...held, until })),
1051
- restriction: wizardScope.rule,
1056
+ restriction: ruleRestriction,
1052
1057
  }}
1053
1058
  name={{
1054
1059
  name: named.name ?? "",
package/src/index.css CHANGED
@@ -4,13 +4,6 @@
4
4
  the .dark ancestor scope used everywhere in this app. */
5
5
  @import "@remit/ui/tokens.css";
6
6
 
7
- /* The structural sheet the calendar grid's engine needs, which the kit ships
8
- beside the component and every app that mounts it has to ask for. Without it
9
- the week and day grids collapse into a stack and draw no events at all: the
10
- engine lays its columns out against this sheet, and a column with no
11
- geometry has nowhere to put an occurrence. */
12
- @import "@remit/ui/calendar.css";
13
-
14
7
  /* Tailwind v4 content scanning: name every source Tailwind must inspect for
15
8
  utility class usage. Automatic detection is rooted at the Vite root, which
16
9
  for the distributor harness build is a throwaway directory holding only the
@@ -0,0 +1,64 @@
1
+ import assert from "node:assert/strict";
2
+ import { readdirSync } from "node:fs";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+ import { describe, it } from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+ import {
7
+ AUTH_COMPOSITIONS,
8
+ bundleAppGraph,
9
+ graphPath,
10
+ } from "@/test-support/test-bundle-app";
11
+
12
+ /**
13
+ * A stylesheet the kit ships beside a component has to reach the app on its
14
+ * own. #1062: `CalendarGrid`'s structural sheet was a subpath every consuming
15
+ * app had to `@import` by hand, this one never did, and three releases drew the
16
+ * week and day views as an unstyled stack with no events in them — while
17
+ * Storybook, the render tests and the server render all read correctly, because
18
+ * each of those pulled the sheet in itself.
19
+ *
20
+ * The kit's token sheet is the exception the app asks for by name: one theme,
21
+ * loaded whether or not anything renders. Every other sheet is structural,
22
+ * belongs to the component that cannot draw without it, and is checked here
23
+ * against the bundler's own module graph walked from the app entry — so a sheet
24
+ * no component imports fails the build instead of a deployment.
25
+ */
26
+
27
+ const here = dirname(fileURLToPath(import.meta.url));
28
+ const kitSrc = resolve(here, "../../ui/src");
29
+
30
+ /** The kit's theme sheet, which `src/index.css` imports directly. */
31
+ const THEME_SHEET = join(kitSrc, "tokens.css");
32
+
33
+ const structuralSheets = (dir: string): string[] => {
34
+ const found: string[] = [];
35
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
36
+ const path = join(dir, entry.name);
37
+ if (entry.isDirectory()) {
38
+ if (entry.name !== "node_modules") found.push(...structuralSheets(path));
39
+ continue;
40
+ }
41
+ if (entry.name.endsWith(".css") && path !== THEME_SHEET) found.push(path);
42
+ }
43
+ return found.sort();
44
+ };
45
+
46
+ describe("kit stylesheets (#1062)", () => {
47
+ it("walks every structural sheet @remit/ui ships from the app entry", async () => {
48
+ const sheets = structuralSheets(kitSrc);
49
+ assert.ok(
50
+ sheets.length > 0,
51
+ "the kit ships no structural sheet, so this asserts nothing — name the sheet it should follow, or drop it",
52
+ );
53
+
54
+ const { inputs } = await bundleAppGraph(AUTH_COMPOSITIONS.betterAuth);
55
+ const reached = new Set(inputs.map(graphPath));
56
+ const orphaned = sheets.filter((sheet) => !reached.has(sheet));
57
+
58
+ assert.deepEqual(
59
+ orphaned.map((sheet) => relative(kitSrc, sheet)),
60
+ [],
61
+ "a structural sheet no component imports never reaches the deployed app",
62
+ );
63
+ });
64
+ });
@@ -0,0 +1,102 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { build } from "esbuild";
4
+
5
+ /**
6
+ * Walks the app the way a distributor's build walks it — from the shell entry,
7
+ * with one identity system composed in — and hands back the module graph the
8
+ * bundler resolved. A suite asserts on that graph rather than on import text: a
9
+ * specifier reads the same whether or not anything resolves it.
10
+ */
11
+
12
+ const here = dirname(fileURLToPath(import.meta.url));
13
+ const srcDir = join(here, "..");
14
+ const packageDir = join(srcDir, "..");
15
+
16
+ /** An identity system a build composes, by the export that supplies it. */
17
+ export interface AuthComposition {
18
+ specifier: string;
19
+ name: string;
20
+ }
21
+
22
+ export const AUTH_COMPOSITIONS: Record<
23
+ "betterAuth" | "cognito",
24
+ AuthComposition
25
+ > = {
26
+ betterAuth: {
27
+ specifier: "@/auth/better-auth-provider",
28
+ name: "betterAuthProvider",
29
+ },
30
+ cognito: {
31
+ specifier: "@/auth/cognito-provider",
32
+ name: "cognitoAuthProvider",
33
+ },
34
+ };
35
+
36
+ export interface AppGraph {
37
+ /** Every file the bundler pulled in, relative to the package root. */
38
+ inputs: string[];
39
+ /** Every specifier those files import, as the bundler resolved it. */
40
+ importPaths: string[];
41
+ }
42
+
43
+ // Externalize third-party packages, but bundle in-repo `@remit/*` workspace
44
+ // source so the walk matches what the real vite build includes. A shared
45
+ // primitive (e.g. `@remit/ui`) that pulled in Amplify would then surface in this
46
+ // graph and fail the test, rather than hiding behind an external edge.
47
+ const externalizeThirdParty = {
48
+ name: "externalize-third-party",
49
+ setup(build: import("esbuild").PluginBuild) {
50
+ build.onResolve({ filter: /.*/ }, (args) => {
51
+ const path = args.path;
52
+ if (path.startsWith(".") || path.startsWith("/")) return null;
53
+ if (path === "@" || path.startsWith("@/")) return null;
54
+ if (path.startsWith("@remit/")) return null;
55
+ return { path, external: true };
56
+ });
57
+ },
58
+ };
59
+
60
+ /** Bundle the app against `auth` and report everything the build reached. */
61
+ export const bundleAppGraph = async (
62
+ auth: AuthComposition,
63
+ ): Promise<AppGraph> => {
64
+ const result = await build({
65
+ stdin: {
66
+ contents: [
67
+ 'import { mountApp } from "@/shell";',
68
+ `import { ${auth.name} } from "${auth.specifier}";`,
69
+ `mountApp({ authProvider: ${auth.name} });`,
70
+ "",
71
+ ].join("\n"),
72
+ resolveDir: packageDir,
73
+ loader: "tsx",
74
+ sourcefile: "compose-entry.tsx",
75
+ },
76
+ bundle: true,
77
+ write: false,
78
+ metafile: true,
79
+ logLevel: "silent",
80
+ format: "esm",
81
+ platform: "browser",
82
+ jsx: "automatic",
83
+ jsxImportSource: "react",
84
+ plugins: [externalizeThirdParty],
85
+ // A stylesheet is emptied rather than parsed: this walk is after which
86
+ // files the build reaches, and what one sheet `@import`s from another is
87
+ // the app stylesheet's business, not the module graph's.
88
+ loader: { ".css": "empty", ".png": "empty", ".svg": "empty" },
89
+ absWorkingDir: packageDir,
90
+ alias: { "@": srcDir },
91
+ });
92
+
93
+ const inputs = Object.keys(result.metafile.inputs);
94
+ const importPaths = new Set<string>();
95
+ for (const input of Object.values(result.metafile.inputs)) {
96
+ for (const imported of input.imports) importPaths.add(imported.path);
97
+ }
98
+ return { inputs, importPaths: [...importPaths] };
99
+ };
100
+
101
+ /** An input path from a graph, as an absolute path on this checkout. */
102
+ export const graphPath = (input: string): string => join(packageDir, input);