@cedarjs/cli 8.0.0-canary.3541 → 8.0.0-canary.3542

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.
@@ -0,0 +1,53 @@
1
+ import j from "jscodeshift";
2
+ const TENANCY_WEB = "@cedarjs/tenancy/web";
3
+ function transform(fileInfo) {
4
+ const root = j.withParser("tsx")(fileInfo.source);
5
+ const routers = root.find(j.JSXOpeningElement, { name: { name: "Router" } });
6
+ if (routers.length !== 1) {
7
+ throw new Error("CEDAR_CODEMOD_ERR_ROUTER_NOT_FOUND");
8
+ }
9
+ const router = routers.get().node;
10
+ if (!j.JSXOpeningElement.check(router)) {
11
+ throw new Error("CEDAR_CODEMOD_ERR_ROUTER_NOT_FOUND");
12
+ }
13
+ const attributes = router.attributes ?? [];
14
+ const hasProp = attributes.some(
15
+ (attribute) => j.JSXAttribute.check(attribute) && attribute.name.name === "useOrgRoles"
16
+ );
17
+ const hasImport = root.find(j.ImportSpecifier, { local: { name: "useOrgRoles" } }).length > 0;
18
+ if (hasProp && hasImport) {
19
+ return fileInfo.source;
20
+ }
21
+ if (!hasProp) {
22
+ const useAuthIndex = attributes.findIndex(
23
+ (attribute) => j.JSXAttribute.check(attribute) && attribute.name.name === "useAuth"
24
+ );
25
+ const prop = j.jsxAttribute(
26
+ j.jsxIdentifier("useOrgRoles"),
27
+ j.jsxExpressionContainer(j.identifier("useOrgRoles"))
28
+ );
29
+ attributes.splice(
30
+ useAuthIndex === -1 ? attributes.length : useAuthIndex + 1,
31
+ 0,
32
+ prop
33
+ );
34
+ router.attributes = attributes;
35
+ }
36
+ if (!hasImport) {
37
+ const specifier = j.importSpecifier(j.identifier("useOrgRoles"));
38
+ const tenancyImport = root.find(j.ImportDeclaration, { source: { value: TENANCY_WEB } }).nodes().find(
39
+ (declaration) => declaration.importKind !== "type" && declaration.specifiers?.every(
40
+ (existing) => j.ImportSpecifier.check(existing)
41
+ )
42
+ );
43
+ if (tenancyImport?.specifiers) {
44
+ tenancyImport.specifiers.push(specifier);
45
+ } else {
46
+ root.find(j.ImportDeclaration).at(-1).insertAfter(j.importDeclaration([specifier], j.literal(TENANCY_WEB)));
47
+ }
48
+ }
49
+ return root.toSource();
50
+ }
51
+ export {
52
+ transform as default
53
+ };
@@ -27,17 +27,26 @@ export interface OrgScopeProps {
27
27
  /**
28
28
  * Wraps organization routes with a per-organization Apollo client and a
29
29
  * membership check, using this app's `useAuth`. Use with the router's
30
- * `Set wrap`:
30
+ * `PrivateSet wrap`, so a user who isn't signed in is sent to log in:
31
31
  *
32
- * <Set wrap={OrgScope}>
32
+ * <PrivateSet wrap={OrgScope} unauthenticated="login">
33
33
  * <Route path="/org/{orgSlug}/projects" page={ProjectsPage} name="projects" />
34
- * </Set>
34
+ * </PrivateSet>
35
35
  *
36
36
  * Under it, `useAuth().hasPermission` and `useCurrentOrg().hasOrgRole`
37
37
  * check the user's membership in the organization, and
38
38
  * `useAuth().hasUserRole` checks `currentUser.roles`. For pages only some
39
- * members may open, wrap them in a component that passes `permission` (or
40
- * `roles`; there's no `<PrivateSet roles>` with tenancy):
39
+ * roles in the organization may open, give the `PrivateSet` `orgRoles`. The
40
+ * router checks the role in the route's organization with the
41
+ * `useOrgRoles` passed to it, `<Router useAuth={useAuth}
42
+ * useOrgRoles={useOrgRoles}>`:
43
+ *
44
+ * <PrivateSet wrap={[OrgScope, OrgLayout]} unauthenticated="login" unauthorized="projects" orgRoles="owner">
45
+ * <Route path="/org/{orgSlug}/settings" page={SettingsPage} name="settings" />
46
+ * </PrivateSet>
47
+ *
48
+ * For pages only members with a permission may open, wrap them in a
49
+ * component that passes `permission`:
41
50
  *
42
51
  * const MemberManagers = ({ children }: { children: ReactNode }) => (
43
52
  * <OrgScope permission="members.manage" unauthorized="projects">
@@ -454,6 +454,28 @@ ${SIGNUP_SNIPPET}`;
454
454
  );
455
455
  }
456
456
  },
457
+ {
458
+ title: "Passing useOrgRoles to the Router in web/src/Routes...",
459
+ task: async (ctx, task) => {
460
+ const routesPath = paths.web.routes;
461
+ if (!routesPath || !fs.existsSync(routesPath)) {
462
+ ctx.routerNeedsUseOrgRoles = true;
463
+ task.skip("No web/src/Routes file found; skipping.");
464
+ return;
465
+ }
466
+ const source = fs.readFileSync(routesPath, "utf-8");
467
+ const transformResult = await runTransform({
468
+ transformPath: path.join(import.meta.dirname, "routerCodemod.js"),
469
+ targetPaths: [routesPath]
470
+ });
471
+ if (transformResult.error) {
472
+ ctx.routerNeedsUseOrgRoles = true;
473
+ task.output = "Could not find the app's <Router> in web/src/Routes, so left it as it is. See the next steps for what to add.";
474
+ } else if (fs.readFileSync(routesPath, "utf-8") !== source) {
475
+ writtenFiles.push(routesPath);
476
+ }
477
+ }
478
+ },
457
479
  {
458
480
  title: "Formatting and linting changed files...",
459
481
  task: async (_ctx, task) => {
@@ -521,16 +543,24 @@ ${SIGNUP_SNIPPET}`;
521
543
  to ${c.highlight("@requireOrgAuth")}. For each one that stays global, add it
522
544
  to the ${c.highlight("allExcept")} list in api/src/lib/db instead.
523
545
  3. Wrap your organization routes with the generated OrgScope in
524
- web/src/Routes.tsx:
546
+ web/src/Routes.tsx, in a PrivateSet, so a user who isn't signed
547
+ in is sent to log in:
525
548
 
526
- <Set wrap={OrgScope}>
549
+ <PrivateSet wrap={OrgScope} unauthenticated="login">
527
550
  <Route path="/org/{orgSlug}/projects" page={ProjectsPage} name="projects" />
528
- </Set>
551
+ </PrivateSet>
529
552
 
530
553
  and add the invitation route outside it:
531
554
 
532
555
  <Route path="/invite/{token}" page={InvitePage} name="invite" />
556
+ ${ctx.routerNeedsUseOrgRoles ? `
557
+ Your ${c.highlight("<Router>")} couldn't be found, so pass it ${c.highlight("useOrgRoles")} by
558
+ hand, for ${c.highlight("<PrivateSet orgRoles>")} below:
559
+
560
+ import { useOrgRoles } from '@cedarjs/tenancy/web'
533
561
 
562
+ <Router useAuth={useAuth} useOrgRoles={useOrgRoles}>
563
+ ` : ""}
534
564
  4. Wrap any plain api/src/functions/* handler that touches
535
565
  tenant-owned models with ${c.highlight("withTenancy")} from ${c.highlight(
536
566
  "@cedarjs/tenancy"
@@ -549,12 +579,19 @@ ${SIGNUP_SNIPPET}`;
549
579
  it and your @requireAuth fields were left as they are: move
550
580
  its checks to requireUserAuth or requireOrgAuth, switch the
551
581
  fields over, then delete it.` : ""}
552
- 6. Every role check names its kind now. ${c.highlight("hasRole")} is
582
+ 7. Every role check names its kind now. ${c.highlight("hasRole")} is
553
583
  ${c.highlight("hasUserRole")} on both sides (your existing uses were renamed),
554
584
  checking ${c.highlight("currentUser.roles")}; ${c.highlight("hasOrgRole")} checks the role in
555
585
  the organization. ${c.highlight("<PrivateSet roles>")} isn't available with
556
- tenancy: guard routes by role with ${c.highlight("OrgScope")}'s ${c.highlight("roles")} and
557
- ${c.highlight("unauthorized")}, from a small wrapper component.
586
+ tenancy: guard routes with ${c.highlight("<PrivateSet userRoles>")} for
587
+ ${c.highlight("currentUser.roles")}, and with ${c.highlight("<PrivateSet orgRoles>")} for the
588
+ role in the route's organization, which the router checks with
589
+ the ${c.highlight("useOrgRoles")} passed to it:
590
+
591
+ <Router useAuth={useAuth} useOrgRoles={useOrgRoles}>
592
+ <PrivateSet wrap={[OrgScope, OrgLayout]} unauthenticated="login" unauthorized="projects" orgRoles="owner">
593
+ <Route path="/org/{orgSlug}/settings" page={SettingsPage} name="settings" />
594
+ </PrivateSet>
558
595
 
559
596
  ${keptFiles.length > 0 ? `
560
597
  Kept these existing files rather than overwriting them (run with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/cli",
3
- "version": "8.0.0-canary.3541",
3
+ "version": "8.0.0-canary.3542",
4
4
  "description": "The CedarJS Command Line",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,17 +36,17 @@
36
36
  "@babel/preset-typescript": "7.29.7",
37
37
  "@babel/traverse": "7.29.10",
38
38
  "@babel/types": "7.29.11",
39
- "@cedarjs/api-server": "8.0.0-canary.3541",
40
- "@cedarjs/babel-config": "8.0.0-canary.3541",
41
- "@cedarjs/cli-helpers": "8.0.0-canary.3541",
42
- "@cedarjs/internal": "8.0.0-canary.3541",
43
- "@cedarjs/prerender": "8.0.0-canary.3541",
44
- "@cedarjs/project-config": "8.0.0-canary.3541",
45
- "@cedarjs/structure": "8.0.0-canary.3541",
46
- "@cedarjs/telemetry": "8.0.0-canary.3541",
47
- "@cedarjs/utils": "8.0.0-canary.3541",
48
- "@cedarjs/vite": "8.0.0-canary.3541",
49
- "@cedarjs/web-server": "8.0.0-canary.3541",
39
+ "@cedarjs/api-server": "8.0.0-canary.3542",
40
+ "@cedarjs/babel-config": "8.0.0-canary.3542",
41
+ "@cedarjs/cli-helpers": "8.0.0-canary.3542",
42
+ "@cedarjs/internal": "8.0.0-canary.3542",
43
+ "@cedarjs/prerender": "8.0.0-canary.3542",
44
+ "@cedarjs/project-config": "8.0.0-canary.3542",
45
+ "@cedarjs/structure": "8.0.0-canary.3542",
46
+ "@cedarjs/telemetry": "8.0.0-canary.3542",
47
+ "@cedarjs/utils": "8.0.0-canary.3542",
48
+ "@cedarjs/vite": "8.0.0-canary.3542",
49
+ "@cedarjs/web-server": "8.0.0-canary.3542",
50
50
  "@listr2/prompt-adapter-enquirer": "4.4.3",
51
51
  "@opentelemetry/api": "1.9.1",
52
52
  "@opentelemetry/core": "2.12.0",
@@ -93,7 +93,7 @@
93
93
  "yargs": "18.2.0"
94
94
  },
95
95
  "devDependencies": {
96
- "@cedarjs/framework-tools": "8.0.0-canary.3541",
96
+ "@cedarjs/framework-tools": "8.0.0-canary.3542",
97
97
  "@prisma/dmmf": "7.10.1",
98
98
  "@types/archiver": "^7.0.0",
99
99
  "memfs": "4.80.0",