rimecms 0.31.4 → 0.31.5

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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter-sqlite/transform.server.js +1 -1
  3. package/dist/core/collections/upload/util/path.d.ts +2 -1
  4. package/dist/core/collections/upload/util/path.js +3 -3
  5. package/dist/core/constant.d.ts +10 -0
  6. package/dist/core/constant.js +22 -0
  7. package/dist/core/dev/cli/build/templates.js +2 -0
  8. package/dist/core/dev/cli/init/index.server.js +2 -1
  9. package/dist/core/dev/cli/init/templates.d.ts +6 -4
  10. package/dist/core/dev/cli/init/templates.js +20 -9
  11. package/dist/core/dev/constants.d.ts +5 -0
  12. package/dist/core/dev/constants.js +9 -0
  13. package/dist/core/dev/generate/routes/common.server.js +33 -33
  14. package/dist/core/dev/generate/routes/index.server.d.ts +6 -3
  15. package/dist/core/dev/generate/routes/index.server.js +13 -7
  16. package/dist/core/dev/generate/routes/util.server.d.ts +14 -6
  17. package/dist/core/dev/generate/routes/util.server.js +17 -6
  18. package/dist/core/handlers/auth.server.js +16 -8
  19. package/dist/core/handlers/routes.server.d.ts +1 -1
  20. package/dist/core/handlers/routes.server.js +4 -3
  21. package/dist/core/operations/hooks/before-read/populate-url.server.js +2 -1
  22. package/dist/core/plugins/cache/handler.server.js +1 -1
  23. package/dist/panel/components/sections/collection/list/row/Row.svelte +2 -1
  24. package/dist/panel/components/sections/collection/tree/CollectionTreeNode.svelte +2 -1
  25. package/dist/panel/components/sections/document/AuthFooter.svelte +2 -1
  26. package/dist/panel/components/sections/document/Versions.svelte +1 -1
  27. package/dist/panel/components/ui/card-resource/card-resource.svelte +2 -1
  28. package/dist/panel/components/ui/nav/Nav.svelte +1 -1
  29. package/dist/panel/components/ui/nav/NavItem.svelte +3 -2
  30. package/dist/panel/components/ui/nav/UserButton.svelte +3 -3
  31. package/dist/panel/context/documentForm.svelte.js +5 -6
  32. package/dist/panel/navigation.d.ts +7 -2
  33. package/dist/panel/navigation.js +11 -6
  34. package/dist/panel/pages/area/actions.server.js +4 -3
  35. package/dist/panel/pages/area/load.server.js +3 -2
  36. package/dist/panel/pages/auth/forgot-password/ForgotPassword.svelte +2 -1
  37. package/dist/panel/pages/auth/reset-password/ResetPassword.svelte +2 -1
  38. package/dist/panel/pages/auth/sign-in/SignIn.svelte +3 -2
  39. package/dist/panel/pages/auth/sign-in/actions.server.js +2 -2
  40. package/dist/panel/pages/auth/sign-in/load.server.d.ts +1 -1
  41. package/dist/panel/pages/auth/sign-in/load.server.js +2 -2
  42. package/dist/panel/pages/collection/load.server.js +9 -4
  43. package/dist/panel/pages/collection-document/actions.server.js +5 -3
  44. package/dist/panel/pages/collection-document/load.server.js +10 -6
  45. package/dist/panel/pages/dashboard/load.server.js +5 -4
  46. package/dist/panel/pages/live/Live.svelte +3 -7
  47. package/dist/panel/util/url.d.ts +30 -1
  48. package/dist/panel/util/url.js +36 -2
  49. package/package.json +1 -1
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ![rime logo](https://github.com/bienbiendev/rime/blob/main/assets/logo-white.png#gh-dark-mode-only)
2
2
  ![rime logo](https://github.com/bienbiendev/rime/blob/main/assets/logo-black.png#gh-light-mode-only)
3
3
 
4
- Headless CMS powered by SvelteKit. [Read the doc](https://github.com/bienbiendev/rime-doc/tree/master/docs)
4
+ Headless CMS powered by SvelteKit. [Read the doc](https://github.com/bienbiendev/rime-doc/blob/develop/docs/02-installation.md)
5
5
 
6
6
  ![NPM Version](https://img.shields.io/npm/v/rimecms)
7
7
  ![NPM Downloads](https://img.shields.io/npm/dm/rimecms)
@@ -24,7 +24,7 @@ export const transformerFacade = (args) => {
24
24
  const tableName = isVersioned ? withVersionsSuffix(slug) : slug;
25
25
  const tableNameRelationFields = `${tableName}Rels`;
26
26
  const tableNameLocales = withLocalesSuffix(tableName);
27
- const isPanel = event.url.pathname.startsWith('/panel');
27
+ const isPanel = event.params.panel !== undefined;
28
28
  let docAPI;
29
29
  if (configCtx.isCollection(slug)) {
30
30
  docAPI = rime.collection(slug);
@@ -46,9 +46,10 @@ export declare function getParentPath(path: UploadPath): string | null;
46
46
  * buildUploadAria('root:foo:bar')
47
47
  * // return [{ title: 'foo', path: '/panel/{slug}?=root:foo' }, { title: 'bar' }]
48
48
  */
49
- export declare function buildUploadAria({ path, slug }: {
49
+ export declare function buildUploadAria({ path, slug, panelSegment }: {
50
50
  path: UploadPath;
51
51
  slug: CollectionSlug;
52
+ panelSegment: string | undefined;
52
53
  }): Partial<Route>[];
53
54
  export declare function removePathFromLastAria(aria: Partial<Route>[]): Partial<Route>[];
54
55
  /**
@@ -1,6 +1,6 @@
1
1
  import { PARAMS, UPLOAD_PATH } from '../../../constant.js';
2
2
  import { RimeError } from '../../../errors/index.js';
3
- import { panelUrl } from '../../../../panel/util/url.js';
3
+ import { panelUrlFor } from '../../../../panel/util/url.js';
4
4
  import { toKebabCase } from '../../../../util/string.js';
5
5
  /**
6
6
  * Validates that a path string follows the required format:
@@ -75,7 +75,7 @@ export function getParentPath(path) {
75
75
  * buildUploadAria('root:foo:bar')
76
76
  * // return [{ title: 'foo', path: '/panel/{slug}?=root:foo' }, { title: 'bar' }]
77
77
  */
78
- export function buildUploadAria({ path, slug }) {
78
+ export function buildUploadAria({ path, slug, panelSegment }) {
79
79
  const segments = path.split(':');
80
80
  const result = [];
81
81
  let currentPath = '';
@@ -86,7 +86,7 @@ export function buildUploadAria({ path, slug }) {
86
86
  if (currentPath !== segments[0]) {
87
87
  result.push({
88
88
  title: segment,
89
- url: `${panelUrl(toKebabCase(slug))}?${PARAMS.UPLOAD_PATH}=${currentPath}`
89
+ url: `${panelUrlFor(panelSegment, toKebabCase(slug))}?${PARAMS.UPLOAD_PATH}=${currentPath}`
90
90
  });
91
91
  }
92
92
  }
@@ -1,3 +1,13 @@
1
+ /** The generated panel route always lives at this fixed matcher-folder id — the literal
2
+ * folder name never changes, only which segment value src/params/panel.ts's matcher
3
+ * accepts (see PANEL_ROUTE in core/dev/constants.ts). Route ids never carry the resolved
4
+ * param value, so these are safe to compare against event.route.id without leaking the
5
+ * configured secret. */
6
+ export declare const PANEL_ROUTE_ID = "/(rime)/[panel=panel]";
7
+ export declare const PANEL_SIGN_IN_ROUTE_ID = "/(rime)/[panel=panel]/sign-in";
8
+ export declare const PANEL_FORGOT_PASSWORD_ROUTE_ID = "/(rime)/[panel=panel]/forgot-password";
9
+ export declare const PANEL_RESET_PASSWORD_ROUTE_ID = "/(rime)/[panel=panel]/reset-password";
10
+ export declare function isPublicPanelAuthRoute(routeId: string | null): boolean;
1
11
  export declare const VERSIONS_STATUS: {
2
12
  readonly DRAFT: "draft";
3
13
  readonly PUBLISHED: "published";
@@ -1,3 +1,25 @@
1
+ /** The generated panel route always lives at this fixed matcher-folder id — the literal
2
+ * folder name never changes, only which segment value src/params/panel.ts's matcher
3
+ * accepts (see PANEL_ROUTE in core/dev/constants.ts). Route ids never carry the resolved
4
+ * param value, so these are safe to compare against event.route.id without leaking the
5
+ * configured secret. */
6
+ export const PANEL_ROUTE_ID = '/(rime)/[panel=panel]';
7
+ export const PANEL_SIGN_IN_ROUTE_ID = `${PANEL_ROUTE_ID}/sign-in`;
8
+ export const PANEL_FORGOT_PASSWORD_ROUTE_ID = `${PANEL_ROUTE_ID}/forgot-password`;
9
+ export const PANEL_RESET_PASSWORD_ROUTE_ID = `${PANEL_ROUTE_ID}/reset-password`;
10
+ /** sign-in, forgot-password and reset-password all have to stay reachable by an
11
+ * unauthenticated visitor (someone locked out, or clicking a reset link from email) even
12
+ * though they now live under the same hidden [panel=panel] segment as the rest of the
13
+ * panel — this is the auth-gate's "public, no-session-required" exception, broadened from
14
+ * sign-in alone to cover all three. */
15
+ const PANEL_PUBLIC_AUTH_ROUTE_IDS = new Set([
16
+ PANEL_SIGN_IN_ROUTE_ID,
17
+ PANEL_FORGOT_PASSWORD_ROUTE_ID,
18
+ PANEL_RESET_PASSWORD_ROUTE_ID
19
+ ]);
20
+ export function isPublicPanelAuthRoute(routeId) {
21
+ return !!routeId && PANEL_PUBLIC_AUTH_ROUTE_IDS.has(routeId);
22
+ }
1
23
  export const VERSIONS_STATUS = {
2
24
  DRAFT: 'draft',
3
25
  PUBLISHED: 'published'
@@ -1,9 +1,11 @@
1
+ import { PANEL_ROUTE } from '../../constants.js';
1
2
  import { randomId } from '../../../../util/random.js';
2
3
  export const envProduction = () => `# BETTER-AUTH
3
4
  BETTER_AUTH_SECRET=${randomId(32)}
4
5
 
5
6
  # RIME
6
7
  PUBLIC_RIME_URL=http://localhost:3000
8
+ RIME_PANEL_ROUTE=${PANEL_ROUTE}
7
9
  RIME_LOG_LEVEL=ERROR
8
10
  RIME_LOG_TO_FILE=true
9
11
  RIME_CACHE_ENABLED=true
@@ -9,7 +9,7 @@ import { cp, mkdir } from 'fs/promises';
9
9
  import fs from 'node:fs';
10
10
  import path from 'path';
11
11
  import { fileURLToPath } from 'url';
12
- import { CONFIG_DIR, OUTPUT_DIR } from '../../constants.js';
12
+ import { CONFIG_DIR, OUTPUT_DIR, PANEL_ROUTE } from '../../constants.js';
13
13
  import { generate } from '../generate/index.server.js';
14
14
  import { installDependencies } from '../util/package-manager.server.js';
15
15
  import { getPackageInfoByKey } from '../util/package.server.js';
@@ -26,6 +26,7 @@ export const init = async ({ force, name: incomingName, skipInstall }) => {
26
26
  BETTER_AUTH_SECRET: randomId(32),
27
27
  PUBLIC_RIME_URL: 'http://localhost:5173',
28
28
  RIME_CONFIG_DIR: CONFIG_DIR,
29
+ RIME_PANEL_ROUTE: PANEL_ROUTE,
29
30
  '# RIME_SMTP_USER': 'user@mail.com',
30
31
  '# RIME_SMTP_PASSWORD': 'supersecret',
31
32
  '# RIME_SMTP_PORT': '465',
@@ -2,10 +2,12 @@ export declare const env: () => string;
2
2
  export declare const defaultConfig: (name: string) => string;
3
3
  export declare const drizzleConfig: (name: string) => string;
4
4
  export declare const hooks: () => string;
5
- /** Writes src/hooks.server.ts if missing, or rewrites it if its content would differ from
6
- * what's expected now (e.g. after RIME_CONFIG_DIR changes the import path) — called both at
7
- * `rime init` and whenever the dev server detects a config change, so it never goes stale
8
- * between the two. Returns whether it wrote anything. */
5
+ /** Writes src/hooks.server.ts if missing. If it already exists, patches only the config
6
+ * import line if it's stale (e.g. after RIME_CONFIG_DIR changes the import path) — the rest
7
+ * of the file may carry real customizations (e.g. an appended handler), so this never
8
+ * touches anything else. Called both at `rime init` and whenever the dev server detects a
9
+ * config change, so the import path never goes stale between the two. Returns whether it
10
+ * wrote anything. */
9
11
  export declare function regenerateHooks(root?: string): boolean;
10
12
  /** Patches drizzle.config.ts's schema path line if it's stale (e.g. after RIME_CONFIG_DIR
11
13
  * changes) — only that one line, since the rest of the file may carry real customizations
@@ -1,12 +1,19 @@
1
1
  import { randomId } from '../../../../util/random.js';
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import path from 'node:path';
4
- import { CONFIG_DIR, GENERATED_DIR, configImportPaths } from '../../constants.js';
4
+ import { CONFIG_DIR, GENERATED_DIR, PANEL_ROUTE, configImportPaths } from '../../constants.js';
5
5
  const PACKAGE = 'rimecms';
6
6
  export const env = () => `BETTER_AUTH_SECRET=${randomId(32)}
7
7
  PUBLIC_RIME_URL=http://localhost:5173
8
8
  RIME_CONFIG_DIR=${CONFIG_DIR}
9
9
 
10
+ # Panel URL segment (e.g. /panel/...). Change it to hide the panel from
11
+ # automated CMS-admin scanners — requests to the old segment 404 instead of
12
+ # revealing a login page. Run 'rime generate' (or restart 'rime dev') after
13
+ # changing this — the dev server only regenerates routes when it detects a
14
+ # change under RIME_CONFIG_DIR, not on every restart.
15
+ RIME_PANEL_ROUTE=${PANEL_ROUTE}
16
+
10
17
  # RIME_CACHE_ENABLED=false
11
18
  # RIME_SMTP_USER=user@mail.com
12
19
  # RIME_SMTP_PASSWORD=supersecret
@@ -63,23 +70,27 @@ import config from '${configImportPaths(HOOKS_DIR).server}';
63
70
 
64
71
  export const handle = sequence(...(await handlers(config)));
65
72
  `;
66
- /** Writes src/hooks.server.ts if missing, or rewrites it if its content would differ from
67
- * what's expected now (e.g. after RIME_CONFIG_DIR changes the import path) — called both at
68
- * `rime init` and whenever the dev server detects a config change, so it never goes stale
69
- * between the two. Returns whether it wrote anything. */
73
+ const HOOKS_CONFIG_IMPORT_LINE_REGEX = /^import config from ['"][^'"]*['"];\s*$/m;
74
+ /** Writes src/hooks.server.ts if missing. If it already exists, patches only the config
75
+ * import line if it's stale (e.g. after RIME_CONFIG_DIR changes the import path) — the rest
76
+ * of the file may carry real customizations (e.g. an appended handler), so this never
77
+ * touches anything else. Called both at `rime init` and whenever the dev server detects a
78
+ * config change, so the import path never goes stale between the two. Returns whether it
79
+ * wrote anything. */
70
80
  export function regenerateHooks(root = process.cwd()) {
71
81
  const hooksPath = path.join(root, 'src', 'hooks.server.ts');
72
82
  const srcDir = path.join(root, 'src');
73
- const content = hooks();
74
83
  if (!existsSync(hooksPath)) {
75
84
  if (!existsSync(srcDir)) {
76
85
  mkdirSync(srcDir, { recursive: true });
77
86
  }
78
- writeFileSync(hooksPath, content, 'utf-8');
87
+ writeFileSync(hooksPath, hooks(), 'utf-8');
79
88
  return true;
80
89
  }
81
- if (readFileSync(hooksPath, 'utf-8') !== content) {
82
- writeFileSync(hooksPath, content, 'utf-8');
90
+ const expectedImportLine = `import config from '${configImportPaths(HOOKS_DIR).server}';`;
91
+ const content = readFileSync(hooksPath, 'utf-8');
92
+ if (HOOKS_CONFIG_IMPORT_LINE_REGEX.test(content) && !content.includes(expectedImportLine)) {
93
+ writeFileSync(hooksPath, content.replace(HOOKS_CONFIG_IMPORT_LINE_REGEX, expectedImportLine));
83
94
  return true;
84
95
  }
85
96
  return false;
@@ -2,6 +2,11 @@ export declare const CONFIG_DIR: string;
2
2
  export declare const GENERATED_DIR: string;
3
3
  export declare const INPUT_DIR: string;
4
4
  export declare const OUTPUT_DIR: string;
5
+ /** The panel's URL segment (e.g. "panel" -> /panel/...). Kept as a bare segment, never a
6
+ * PUBLIC_-prefixed env var: SvelteKit ships every PUBLIC_ value into the JS bundle of every
7
+ * page (including the public site), which would defeat the point of letting an operator hide
8
+ * this path from automated CMS-admin scanners. */
9
+ export declare const PANEL_ROUTE: string;
5
10
  /** Turns an absolute path into an import specifier relative to `fromDir`, always prefixed
6
11
  * with `./` or `../` so it never gets mistaken for a bare package specifier. */
7
12
  export declare function relativeImportSpecifier(fromDir: string, toAbsolutePath: string): string;
@@ -1,5 +1,6 @@
1
1
  import dotenv from 'dotenv';
2
2
  import path from 'node:path';
3
+ import { isValidSlug } from '../../util/string.js';
3
4
  // Must run before RIME_CONFIG_DIR is read below. ES import hoisting evaluates this module's
4
5
  // top-level code before any *importing* file's own top-level statements — including a
5
6
  // dotenv.config() call there — so loading .env here, first, is the only way every entry point
@@ -14,6 +15,14 @@ export const CONFIG_DIR = process.env.RIME_CONFIG_DIR || 'src/+rime';
14
15
  export const GENERATED_DIR = `${CONFIG_DIR}.generated`;
15
16
  export const INPUT_DIR = path.basename(CONFIG_DIR);
16
17
  export const OUTPUT_DIR = `${INPUT_DIR}.generated`;
18
+ /** The panel's URL segment (e.g. "panel" -> /panel/...). Kept as a bare segment, never a
19
+ * PUBLIC_-prefixed env var: SvelteKit ships every PUBLIC_ value into the JS bundle of every
20
+ * page (including the public site), which would defeat the point of letting an operator hide
21
+ * this path from automated CMS-admin scanners. */
22
+ export const PANEL_ROUTE = (process.env.RIME_PANEL_ROUTE || 'panel').replace(/^\/+|\/+$/g, '');
23
+ if (!isValidSlug(PANEL_ROUTE)) {
24
+ throw new Error(`RIME_PANEL_ROUTE must be a single URL segment (letters, numbers, "_"/"-", starting with a letter) — got "${PANEL_ROUTE}"`);
25
+ }
17
26
  const RESERVED_DIRS = new Set(['lib', 'routes', 'static', 'params'].map((d) => path.resolve(SRC_ROOT, d)));
18
27
  for (const dir of [CONFIG_DIR, GENERATED_DIR]) {
19
28
  const resolved = path.resolve(root, dir);
@@ -1,8 +1,8 @@
1
1
  import { PACKAGE_NAME } from '../../../constant.server.js';
2
2
  import path from 'node:path';
3
3
  import { configImportPaths } from '../../constants.js';
4
- const PANEL_LAYOUT_DIR = path.resolve(process.cwd(), 'src/routes/(rime)/panel');
5
- const LIVE_PAGE_DIR = path.resolve(process.cwd(), 'src/routes/(rime)/panel/live-edit');
4
+ const PANEL_LAYOUT_DIR = path.resolve(process.cwd(), 'src/routes/(rime)/[panel=panel]');
5
+ const LIVE_PAGE_DIR = path.resolve(process.cwd(), 'src/routes/(rime)/[panel=panel]/live-edit');
6
6
  /**
7
7
  * Main base layout
8
8
  * /+layout.server.ts
@@ -82,7 +82,7 @@ export const load = async ({ locals }: ServerLoadEvent) => {
82
82
  };`;
83
83
  /**
84
84
  * Login page template
85
- * (rime)/panel/sign-in/+page.svelte
85
+ * (rime)/[panel=panel]/sign-in/+page.svelte
86
86
  */
87
87
  const signInPage = () => `
88
88
  <script>
@@ -105,7 +105,7 @@ export const actions = {
105
105
  `;
106
106
  /**
107
107
  * Forgot password page template
108
- * (rime)/forgot-password/+page.svelte
108
+ * (rime)/[panel=panel]/forgot-password/+page.svelte
109
109
  */
110
110
  const forgotPasswordPage = () => `
111
111
  <script>
@@ -115,7 +115,7 @@ const forgotPasswordPage = () => `
115
115
  <ForgotPassword {data} />`;
116
116
  /**
117
117
  * Forgot password page server template
118
- * (rime)/forgot-password/+page.server.ts
118
+ * (rime)/[panel=panel]/forgot-password/+page.server.ts
119
119
  */
120
120
  const forgotPasswordPageServer = () => `
121
121
  import { type ServerLoadEvent } from '@sveltejs/kit';
@@ -124,7 +124,7 @@ export const load = (event: ServerLoadEvent) => event.locals.routes.panel.load.f
124
124
  `;
125
125
  /**
126
126
  * Reset password page template
127
- * (rime)/reset-password/+page.svelte
127
+ * (rime)/[panel=panel]/reset-password/+page.svelte
128
128
  */
129
129
  const resetPasswordPage = () => `
130
130
  <script>
@@ -135,7 +135,7 @@ const resetPasswordPage = () => `
135
135
  <ResetPassword {data} />`;
136
136
  /**
137
137
  * Reset password page server template
138
- * (rime)/reset-password/+page.server.ts
138
+ * (rime)/[panel=panel]/reset-password/+page.server.ts
139
139
  */
140
140
  const resetPasswordPageServer = () => `
141
141
  import { type ServerLoadEvent } from '@sveltejs/kit';
@@ -144,7 +144,7 @@ export const load = (event: ServerLoadEvent) => event.locals.routes.panel.load.r
144
144
  `;
145
145
  /**
146
146
  * Panel layout template
147
- * (rime)/panel/+layout.svelte
147
+ * (rime)/[panel=panel]/+layout.svelte
148
148
  */
149
149
  const panelLayout = () => `
150
150
  <script>
@@ -164,7 +164,7 @@ const panelLayout = () => `
164
164
  </Panel>`;
165
165
  /**
166
166
  * Panel layout server template
167
- * (rime)/panel/+layout.server.ts
167
+ * (rime)/[panel=panel]/+layout.server.ts
168
168
  */
169
169
  const panelLayoutServer = () => `
170
170
  import { type ServerLoadEvent } from '@sveltejs/kit';
@@ -175,7 +175,7 @@ export const load = async ({ locals }: ServerLoadEvent) => {
175
175
  };`;
176
176
  /**
177
177
  * Panel page template
178
- * (rime)/panel/+page.svelte
178
+ * (rime)/[panel=panel]/+page.svelte
179
179
  */
180
180
  const panelPage = () => `
181
181
  <script>
@@ -186,7 +186,7 @@ const panelPage = () => `
186
186
  <Dashboard entries={data.entries} user={data.user} />`;
187
187
  /**
188
188
  * Panel page load template
189
- * (rime)/panel/+page.server.ts
189
+ * (rime)/[panel=panel]/+page.server.ts
190
190
  */
191
191
  const panelPageServer = () => `
192
192
  import { type ServerLoadEvent } from '@sveltejs/kit';
@@ -229,7 +229,7 @@ export const load = (event: ServerLoadEvent) => event.locals.routes.panel.load.l
229
229
  /****************************************************/
230
230
  /**
231
231
  * Collection list page
232
- * (rime)/panel/[slug=collection]/+page.svelte
232
+ * (rime)/[panel=panel]/[slug=collection]/+page.svelte
233
233
  */
234
234
  const collectionListPage = () => `
235
235
  <script>
@@ -239,7 +239,7 @@ const collectionListPage = () => `
239
239
 
240
240
  <Collection {data} slug={data.slug} />`;
241
241
  /**
242
- * (rime)/panel/[slug=collection]/+page.server.ts
242
+ * (rime)/[panel=panel]/[slug=collection]/+page.server.ts
243
243
  */
244
244
  const collectionListPageServer = () => `
245
245
  import { type ServerLoadEvent } from '@sveltejs/kit';
@@ -250,7 +250,7 @@ export const load = async (event: ServerLoadEvent) => {
250
250
  };`;
251
251
  /**
252
252
  * Area document page
253
- * (rime)/panel/[slug=area]/+page.svelte
253
+ * (rime)/[panel=panel]/[slug=area]/+page.svelte
254
254
  */
255
255
  const areaDocPage = () => `
256
256
  <script>
@@ -260,7 +260,7 @@ const areaDocPage = () => `
260
260
 
261
261
  <Area {data} />`;
262
262
  /**
263
- * (rime)/panel/[slug=area]/+page.server.ts
263
+ * (rime)/[panel=panel]/[slug=area]/+page.server.ts
264
264
  */
265
265
  const areaDocPageServer = () => `
266
266
  import type { RequestEvent, ServerLoadEvent } from '@sveltejs/kit';
@@ -272,7 +272,7 @@ export const actions = {
272
272
  };`;
273
273
  /**
274
274
  * Collection document page
275
- * (rime)/panel/[slug=collection]/[id]/+page.svelte
275
+ * (rime)/[panel=panel]/[slug=collection]/[id]/+page.svelte
276
276
  */
277
277
  const collectionDocPage = () => `
278
278
  <script>
@@ -282,7 +282,7 @@ const collectionDocPage = () => `
282
282
 
283
283
  <CollectionDoc {data} />`;
284
284
  /**
285
- * (rime)/panel/[slug=collection]/[id]/+page.server.ts
285
+ * (rime)/[panel=panel]/[slug=collection]/[id]/+page.server.ts
286
286
  */
287
287
  const collectionDocPageServer = () => `
288
288
  import type { RequestEvent, ServerLoadEvent } from '@sveltejs/kit';
@@ -295,7 +295,7 @@ export const actions = {
295
295
  };`;
296
296
  /**
297
297
  * Collection document versions page
298
- * (rime)/panel/[slug=collection]/[id]/versions/+page.svelte
298
+ * (rime)/[panel=panel]/[slug=collection]/[id]/versions/+page.svelte
299
299
  */
300
300
  const collectionDocVersionsPage = () => `
301
301
  <script>
@@ -305,7 +305,7 @@ const collectionDocVersionsPage = () => `
305
305
 
306
306
  <CollectionDocVersions data={data} />`;
307
307
  /**
308
- * (rime)/panel/[slug=collection]/[id]/versions/+page.server.ts
308
+ * (rime)/[panel=panel]/[slug=collection]/[id]/versions/+page.server.ts
309
309
  */
310
310
  const collectionDocVersionsPageServer = () => `
311
311
  import { type ServerLoadEvent } from '@sveltejs/kit';
@@ -314,7 +314,7 @@ export const load = (event: ServerLoadEvent) =>
314
314
  event.locals.routes.panel.load.documentVersions(event);`;
315
315
  /**
316
316
  * Area document versions page
317
- * (rime)/panel/[slug=area]/versions/+page.svelte
317
+ * (rime)/[panel=panel]/[slug=area]/versions/+page.svelte
318
318
  */
319
319
  const areaVersionsPage = () => `
320
320
  <script>
@@ -324,7 +324,7 @@ const areaVersionsPage = () => `
324
324
 
325
325
  <AreaVersionsDoc data={data} />`;
326
326
  /**
327
- * (rime)/panel/[slug=area]/versions/+page.server.ts
327
+ * (rime)/[panel=panel]/[slug=area]/versions/+page.server.ts
328
328
  */
329
329
  const areaVersionsPageServer = () => `
330
330
  import { type ServerLoadEvent } from '@sveltejs/kit';
@@ -439,45 +439,45 @@ export const commonRoutes = {
439
439
  layoutServer: rootLayoutServer,
440
440
  error: error
441
441
  },
442
- '(rime)/panel/sign-in': {
442
+ '(rime)/[panel=panel]/sign-in': {
443
443
  'page@(rime)': signInPage,
444
444
  pageServer: signInPageServer
445
445
  },
446
- '(rime)/forgot-password': {
447
- page: forgotPasswordPage,
446
+ '(rime)/[panel=panel]/forgot-password': {
447
+ 'page@(rime)': forgotPasswordPage,
448
448
  pageServer: forgotPasswordPageServer
449
449
  },
450
- '(rime)/reset-password': {
451
- page: resetPasswordPage,
450
+ '(rime)/[panel=panel]/reset-password': {
451
+ 'page@(rime)': resetPasswordPage,
452
452
  pageServer: resetPasswordPageServer
453
453
  },
454
- '(rime)/panel': {
454
+ '(rime)/[panel=panel]': {
455
455
  layout: panelLayout,
456
456
  layoutServer: panelLayoutServer,
457
457
  page: panelPage,
458
458
  pageServer: panelPageServer
459
459
  },
460
- '(rime)/panel/live-edit': {
460
+ '(rime)/[panel=panel]/live-edit': {
461
461
  'page@(rime)': livePage,
462
462
  pageServer: livePageServer
463
463
  },
464
- '(rime)/panel/[slug=collection]': {
464
+ '(rime)/[panel=panel]/[slug=collection]': {
465
465
  page: collectionListPage,
466
466
  pageServer: collectionListPageServer
467
467
  },
468
- '(rime)/panel/[slug=collection]/[id]': {
468
+ '(rime)/[panel=panel]/[slug=collection]/[id]': {
469
469
  page: collectionDocPage,
470
470
  pageServer: collectionDocPageServer
471
471
  },
472
- '(rime)/panel/[slug=collection]/[id]/versions': {
472
+ '(rime)/[panel=panel]/[slug=collection]/[id]/versions': {
473
473
  page: collectionDocVersionsPage,
474
474
  pageServer: collectionDocVersionsPageServer
475
475
  },
476
- '(rime)/panel/[slug=area]': {
476
+ '(rime)/[panel=panel]/[slug=area]': {
477
477
  page: areaDocPage,
478
478
  pageServer: areaDocPageServer
479
479
  },
480
- '(rime)/panel/[slug=area]/versions': {
480
+ '(rime)/[panel=panel]/[slug=area]/versions': {
481
481
  page: areaVersionsPage,
482
482
  pageServer: areaVersionsPageServer
483
483
  },
@@ -2,11 +2,14 @@ import type { Config } from '../../../../types.js';
2
2
  /**
3
3
  * Main function to generate browser routes based on configuration.
4
4
  * commonRoutes is a fixed set of files (layout, sign-in, dashboard, the
5
- * matcher-disambiguated /panel/[slug=collection|area]/... and
5
+ * matcher-disambiguated [panel=panel]/[slug=collection|area]/... and
6
6
  * /api/[slug=collection|area]/... dynamic routes) — it no longer varies with
7
7
  * how many collections/areas are configured, so there's no per-collection/area
8
- * loop here anymore. The src/params/collection.ts and area.ts matchers backing
9
- * those routes DO bake in the actual slug list, so they're rewritten here too.
8
+ * loop here anymore. The folder is always the literal `[panel=panel]` segment;
9
+ * only src/params/panel.ts's matcher content varies with RIME_PANEL_ROUTE, so
10
+ * the URL itself can be hidden from admin-path scanners without ever renaming
11
+ * a directory. Same idea backs src/params/collection.ts and area.ts, which
12
+ * bake in the actual slug list — all three are rewritten here.
10
13
  */
11
14
  declare function generateRoutes<T extends Config>(config: T): void;
12
15
  export default generateRoutes;
@@ -1,3 +1,4 @@
1
+ import { PANEL_ROUTE } from '../../constants.js';
1
2
  import { logger } from '../../../logger/index.server.js';
2
3
  import fs from 'fs';
3
4
  import path from 'path';
@@ -8,11 +9,14 @@ const projectRoot = process.cwd();
8
9
  /**
9
10
  * Main function to generate browser routes based on configuration.
10
11
  * commonRoutes is a fixed set of files (layout, sign-in, dashboard, the
11
- * matcher-disambiguated /panel/[slug=collection|area]/... and
12
+ * matcher-disambiguated [panel=panel]/[slug=collection|area]/... and
12
13
  * /api/[slug=collection|area]/... dynamic routes) — it no longer varies with
13
14
  * how many collections/areas are configured, so there's no per-collection/area
14
- * loop here anymore. The src/params/collection.ts and area.ts matchers backing
15
- * those routes DO bake in the actual slug list, so they're rewritten here too.
15
+ * loop here anymore. The folder is always the literal `[panel=panel]` segment;
16
+ * only src/params/panel.ts's matcher content varies with RIME_PANEL_ROUTE, so
17
+ * the URL itself can be hidden from admin-path scanners without ever renaming
18
+ * a directory. Same idea backs src/params/collection.ts and area.ts, which
19
+ * bake in the actual slug list — all three are rewritten here.
16
20
  */
17
21
  function generateRoutes(config) {
18
22
  logger.info('Routes generation...');
@@ -23,7 +27,7 @@ function generateRoutes(config) {
23
27
  // 2. Ensure base directories exist
24
28
  const rootRoutes = path.resolve(projectRoot, 'src', 'routes');
25
29
  const rimeRoutes = path.join(rootRoutes, '(rime)');
26
- const panelRoute = path.join(rimeRoutes, 'panel');
30
+ const panelRoute = path.join(rimeRoutes, '[panel=panel]');
27
31
  const paramsDir = path.resolve(projectRoot, 'src', 'params');
28
32
  fs.rmSync(rimeRoutes, { recursive: true, force: true });
29
33
  ensureDir(rootRoutes);
@@ -44,8 +48,10 @@ function generateRoutes(config) {
44
48
  writeRouteFile(rootRoutes, pattern, fileType, templateFn());
45
49
  }
46
50
  }
47
- // 4. Write the [slug=collection]/[slug=area] param matchers, only when needed — and remove any
48
- // stale matcher left over from a previous config that did have collections/areas.
51
+ // 4. Write the [panel=panel] param matcher — always present, unlike collection/area — plus
52
+ // the [slug=collection]/[slug=area] matchers, only when needed, removing any stale matcher
53
+ // left over from a previous config that did have collections/areas.
54
+ fs.writeFileSync(path.join(paramsDir, 'panel.ts'), paramMatcher([PANEL_ROUTE]));
49
55
  const collectionMatcherPath = path.join(paramsDir, 'collection.ts');
50
56
  if (hasCollections) {
51
57
  fs.writeFileSync(collectionMatcherPath, paramMatcher((config.collections || []).map((c) => c.kebab)));
@@ -64,7 +70,7 @@ function generateRoutes(config) {
64
70
  const customRoutes = config.panel?.routes;
65
71
  if (customRoutes) {
66
72
  for (const [route, routeConfig] of Object.entries(customRoutes)) {
67
- const routePath = path.join('(rime)', 'panel', route);
73
+ const routePath = path.join('(rime)', '[panel=panel]', route);
68
74
  writeRouteFile(rootRoutes, routePath, 'page', customRoute(routeConfig));
69
75
  }
70
76
  }
@@ -8,10 +8,18 @@ export type Routes = Record<string, RouteDefinition>;
8
8
  /**
9
9
  * Check if routes need to be regenerated based on config changes.
10
10
  * Panel/API routes are a fixed set of dynamic-segment files (file count never
11
- * grows with schema size), but the [slug=collection]/[slug=area] param
12
- * matchers under src/params/ bake in the actual slug lists, so adding,
13
- * removing, or renaming a collection/area still needs a regen to keep those
14
- * matchers in sync — same trigger as custom routes and panel CSS.
11
+ * grows with schema size), but the [panel=panel]/[slug=collection]/[slug=area]
12
+ * param matchers under src/params/ bake in the actual accepted value(s), so
13
+ * changing RIME_PANEL_ROUTE or RIME_CONFIG_DIR, or adding/removing/renaming a
14
+ * collection/area, still needs a regen to keep those matchers in sync — same
15
+ * trigger as custom routes and panel CSS. Both PANEL_ROUTE and CONFIG_DIR must
16
+ * stay in this memo: they're read once at process start, so a changed value
17
+ * only takes effect after a restart, and only if the restart's regen actually
18
+ * notices the change. CONFIG_DIR specifically is baked into the panel/live
19
+ * layout's config import path (see configImportPaths() calls in
20
+ * common.server.ts) — leaving it out would silently keep that import stale
21
+ * instead of erroring, the same failure mode RIME_CONFIG_DIR hit once for
22
+ * hooks.server.ts (see regenerateHooks() in cli/init/templates.ts).
15
23
  * @returns true if routes should be regenerated, false otherwise
16
24
  */
17
25
  export declare function shouldRegenerateRoutes<T extends Config>(config: T): boolean;
@@ -33,10 +41,10 @@ export declare function ensureDir(dirPath: string): void;
33
41
  * @example
34
42
  * // Write a page file for a collection
35
43
  * const basePath = '/path/to/src/routes';
36
- * const routePath = '(rime)/panel/news';
44
+ * const routePath = '(rime)/[panel=panel]/news';
37
45
  * const fileType = 'page';
38
46
  * const content = '<script>\n import { Collection } from "rime/panel"\n</script>...';
39
47
  * writeRouteFile(basePath, routePath, fileType, content);
40
- * // Creates /path/to/src/routes/(rime)/panel/news/+page.svelte with the provided content
48
+ * // Creates /path/to/src/routes/(rime)/[panel=panel]/news/+page.svelte with the provided content
41
49
  */
42
50
  export declare function writeRouteFile(basePath: string, routePath: string, fileType: string, content: string): void;
@@ -1,18 +1,29 @@
1
1
  import cache from '../../cache/index.server.js';
2
+ import { CONFIG_DIR, PANEL_ROUTE } from '../../constants.js';
2
3
  import { slugify } from '../../../../util/string.js';
3
4
  import fs from 'fs';
4
5
  import path from 'path';
5
6
  /**
6
7
  * Check if routes need to be regenerated based on config changes.
7
8
  * Panel/API routes are a fixed set of dynamic-segment files (file count never
8
- * grows with schema size), but the [slug=collection]/[slug=area] param
9
- * matchers under src/params/ bake in the actual slug lists, so adding,
10
- * removing, or renaming a collection/area still needs a regen to keep those
11
- * matchers in sync — same trigger as custom routes and panel CSS.
9
+ * grows with schema size), but the [panel=panel]/[slug=collection]/[slug=area]
10
+ * param matchers under src/params/ bake in the actual accepted value(s), so
11
+ * changing RIME_PANEL_ROUTE or RIME_CONFIG_DIR, or adding/removing/renaming a
12
+ * collection/area, still needs a regen to keep those matchers in sync — same
13
+ * trigger as custom routes and panel CSS. Both PANEL_ROUTE and CONFIG_DIR must
14
+ * stay in this memo: they're read once at process start, so a changed value
15
+ * only takes effect after a restart, and only if the restart's regen actually
16
+ * notices the change. CONFIG_DIR specifically is baked into the panel/live
17
+ * layout's config import path (see configImportPaths() calls in
18
+ * common.server.ts) — leaving it out would silently keep that import stale
19
+ * instead of erroring, the same failure mode RIME_CONFIG_DIR hit once for
20
+ * hooks.server.ts (see regenerateHooks() in cli/init/templates.ts).
12
21
  * @returns true if routes should be regenerated, false otherwise
13
22
  */
14
23
  export function shouldRegenerateRoutes(config) {
15
24
  const memo = `
25
+ panel:${PANEL_ROUTE}
26
+ config:${CONFIG_DIR}
16
27
  custom:${config.panel?.routes
17
28
  ? Object.entries(config.panel.routes)
18
29
  .map(([k, v]) => `${k}-${slugify(v.component.toString())}`)
@@ -51,11 +62,11 @@ export function ensureDir(dirPath) {
51
62
  * @example
52
63
  * // Write a page file for a collection
53
64
  * const basePath = '/path/to/src/routes';
54
- * const routePath = '(rime)/panel/news';
65
+ * const routePath = '(rime)/[panel=panel]/news';
55
66
  * const fileType = 'page';
56
67
  * const content = '<script>\n import { Collection } from "rime/panel"\n</script>...';
57
68
  * writeRouteFile(basePath, routePath, fileType, content);
58
- * // Creates /path/to/src/routes/(rime)/panel/news/+page.svelte with the provided content
69
+ * // Creates /path/to/src/routes/(rime)/[panel=panel]/news/+page.svelte with the provided content
59
70
  */
60
71
  export function writeRouteFile(basePath, routePath, fileType, content) {
61
72
  const dir = path.join(basePath, routePath);