rimecms 0.30.3 → 0.31.0

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 (84) hide show
  1. package/README.md +1 -39
  2. package/dist/.rime-modules.d.ts +23 -0
  3. package/dist/.rime-modules.json +1 -0
  4. package/dist/adapter-sqlite/generate-schema/write.server.js +6 -6
  5. package/dist/core/areas/config/builder.server.d.ts +0 -1
  6. package/dist/core/collections/auth/hooks/before-upsert/augment-fields-password.server.d.ts +1 -1
  7. package/dist/core/collections/config/builder.server.d.ts +0 -1
  8. package/dist/core/collections/upload/hooks/convert-base64.server.d.ts +1 -1
  9. package/dist/core/collections/upload/hooks/extract-path.server.d.ts +1 -1
  10. package/dist/core/collections/upload/hooks/handle-path-creation.server.d.ts +1 -1
  11. package/dist/core/collections/upload/hooks/process-file-upload.server.d.ts +1 -1
  12. package/dist/core/config/auth/better-auth.server.d.ts +3 -3
  13. package/dist/core/config/client/augment-plugins.d.ts +1 -5
  14. package/dist/core/config/client/augment-plugins.js +3 -3
  15. package/dist/core/config/client/build-config.d.ts +1 -5
  16. package/dist/core/config/config-context.server.d.ts +1 -1
  17. package/dist/core/config/server/augment-plugins.server.d.ts +2 -42
  18. package/dist/core/config/server/augment-plugins.server.js +11 -6
  19. package/dist/core/config/server/build-config.server.d.ts +9 -4007
  20. package/dist/core/config/types.d.ts +12 -11
  21. package/dist/core/dev/cache/index.server.js +1 -1
  22. package/dist/core/dev/cli/clear/index.server.js +2 -0
  23. package/dist/core/dev/cli/generate-manifest/index.server.d.ts +17 -0
  24. package/dist/core/dev/cli/generate-manifest/index.server.js +221 -0
  25. package/dist/core/dev/cli/index.js +42 -0
  26. package/dist/core/dev/cli/init/index.server.js +11 -4
  27. package/dist/core/dev/cli/init/templates.d.ts +1 -1
  28. package/dist/core/dev/cli/init/templates.js +5 -3
  29. package/dist/core/dev/cli/util/package-manager.server.js +5 -3
  30. package/dist/core/dev/constants.d.ts +10 -0
  31. package/dist/core/dev/constants.js +15 -0
  32. package/dist/core/dev/generate/routes/common.server.js +4 -7
  33. package/dist/core/dev/generate/routes/index.server.js +25 -4
  34. package/dist/core/dev/generate/runtime/index.server.d.ts +31 -15
  35. package/dist/core/dev/generate/runtime/index.server.js +45 -53
  36. package/dist/core/dev/generate/runtime/parse-exports.server.d.ts +7 -0
  37. package/dist/core/dev/generate/runtime/parse-exports.server.js +40 -0
  38. package/dist/core/dev/generate/sanitize/index.server.d.ts +0 -7
  39. package/dist/core/dev/generate/sanitize/index.server.js +57 -5
  40. package/dist/core/dev/generate/types/index.server.js +16 -39
  41. package/dist/core/dev/vite/index.server.js +217 -39
  42. package/dist/core/ensure.server.d.ts +4 -3
  43. package/dist/core/ensure.server.js +8 -8
  44. package/dist/core/handlers/index.js +2 -2
  45. package/dist/core/operations/hooks/before-upsert/data-config-map.server.d.ts +1 -1
  46. package/dist/core/operations/hooks/before-upsert/handle-new-version.server.d.ts +1 -1
  47. package/dist/core/operations/hooks/before-upsert/original-config-map.server.d.ts +1 -1
  48. package/dist/core/operations/hooks/before-upsert/set-default-values.server.d.ts +1 -1
  49. package/dist/core/operations/hooks/before-upsert/validate-fields.server.d.ts +1 -1
  50. package/dist/core/operations/hooks/index.server.d.ts +0 -4
  51. package/dist/core/operations/hooks/index.server.js +0 -4
  52. package/dist/core/plugins/api-init/index.d.ts +2 -0
  53. package/dist/core/plugins/api-init/index.js +1 -0
  54. package/dist/core/plugins/api-init/{index.server.d.ts → module.server.d.ts} +0 -1
  55. package/dist/core/plugins/api-init/{index.server.js → module.server.js} +0 -1
  56. package/dist/core/plugins/cache/index.d.ts +2 -5
  57. package/dist/core/plugins/cache/index.js +1 -21
  58. package/dist/core/plugins/cache/module.d.ts +4 -0
  59. package/dist/core/plugins/cache/module.js +22 -0
  60. package/dist/core/plugins/cache/{index.server.d.ts → module.server.d.ts} +1 -2
  61. package/dist/core/plugins/cache/{index.server.js → module.server.js} +0 -1
  62. package/dist/core/plugins/index.d.ts +1 -8
  63. package/dist/core/plugins/index.js +0 -3
  64. package/dist/core/plugins/mailer/index.d.ts +2 -0
  65. package/dist/core/plugins/mailer/index.js +1 -0
  66. package/dist/core/plugins/mailer/{index.server.d.ts → module.server.d.ts} +0 -1
  67. package/dist/core/plugins/mailer/{index.server.js → module.server.js} +0 -1
  68. package/dist/core/plugins/sse/index.d.ts +2 -0
  69. package/dist/core/plugins/sse/index.js +1 -0
  70. package/dist/core/plugins/sse/{index.server.d.ts → module.server.d.ts} +0 -1
  71. package/dist/core/plugins/sse/{index.server.js → module.server.js} +0 -1
  72. package/dist/core/rime.server.d.ts +16 -16
  73. package/dist/fields/link/index.js +59 -56
  74. package/dist/fields/relation/index.js +63 -70
  75. package/dist/index.d.ts +3 -3
  76. package/dist/index.js +3 -3
  77. package/dist/panel/context/config.svelte.d.ts +2 -2
  78. package/dist/panel/context/documentForm.svelte.d.ts +2 -2
  79. package/dist/panel/context/live.svelte.js +1 -1
  80. package/dist/panel/pages/auth/sign-in/SignIn.svelte +6 -1
  81. package/dist/server.d.ts +4 -4
  82. package/dist/server.js +4 -4
  83. package/dist/types.d.ts +1 -1
  84. package/package.json +2 -2
@@ -1,8 +1,8 @@
1
1
  import type { Adapter } from '../../adapter-sqlite/index.server.js';
2
2
  import type { PanelLanguage } from '../i18n/index.js';
3
3
  import type { Hook, HookBeforeOperation } from '../operations/hooks/index.server.js';
4
- import type { Plugin, PluginClient } from '../plugins/index.js';
5
- import type { SMTPConfig } from '../plugins/mailer/index.server.js';
4
+ import type { Plugin } from '../plugins/index.js';
5
+ import type { SMTPConfig } from '../plugins/mailer/module.server.js';
6
6
  import type { Field, Option } from '../../fields/types.js';
7
7
  import type { RegisterArea, RegisterCollection } from '../../index.js';
8
8
  import type { DashboardEntry } from '../../panel/pages/dashboard/types.js';
@@ -79,10 +79,9 @@ export interface Config {
79
79
  * }
80
80
  */
81
81
  $routes?: Record<string, RouteConfig>;
82
- /** List of client plugins */
83
- plugins?: PluginClient[];
84
- /** List of server plugins */
85
- $plugins?: Plugin[];
82
+ /** List of plugins — safety for anything server-only inside one comes from the
83
+ * $rime/.../module convention, not from a $-prefix on this key */
84
+ plugins?: Plugin[];
86
85
  /** Custom object for server-only config additional values */
87
86
  $custom?: Record<string, any>;
88
87
  /** Custom object for both client and server config additional values */
@@ -309,7 +308,7 @@ export type BuiltCollection = Omit<Collection<string>, 'icon' | 'versions' | 'up
309
308
  _generateSchema?: false;
310
309
  _generateRoutes?: false;
311
310
  };
312
- export type BuiltAreaClient = Omit<BuiltArea, '$url' | '$hooks'>;
311
+ export type BuiltAreaClient = BuiltArea;
313
312
  export type BuiltArea = Omit<Area<string>, 'versions'> & {
314
313
  slug: AreaSlug;
315
314
  type: 'area';
@@ -326,7 +325,7 @@ export type BuiltArea = Omit<Area<string>, 'versions'> & {
326
325
  _generateSchema?: false;
327
326
  _generateRoutes?: false;
328
327
  };
329
- export type BuiltCollectionClient = Omit<BuiltCollection, '$url' | '$hooks'>;
328
+ export type BuiltCollectionClient = BuiltCollection;
330
329
  export type BuiltConfig = {
331
330
  /** Database location relative to the root project ex: ./db/my-app.sqlite */
332
331
  $database: string;
@@ -341,8 +340,10 @@ export type BuiltConfig = {
341
340
  icons: Record<string, any>;
342
341
  $trustedOrigins: string[];
343
342
  $routes?: Record<string, RouteConfig>;
343
+ /** Fully-resolved plugin list, computed by augmentPluginsServer — internal, not the
344
+ * consumer-facing config key (that's just `plugins`) */
344
345
  $plugins?: Plugin[];
345
- plugins?: PluginClient[];
346
+ plugins?: Plugin[];
346
347
  panel: {
347
348
  routes: Record<string, CustomPanelRoute>;
348
349
  navigation: NavigationConfig;
@@ -366,12 +367,12 @@ export type BuiltConfig = {
366
367
  $custom?: Record<string, any>;
367
368
  custom?: Record<string, any>;
368
369
  };
369
- export type ServerConfigProps = '$adapter' | '$database' | '$trustedOrigins' | '$routes' | '$smtp' | '$custom' | '$plugins' | '$auth';
370
+ export type ServerConfigProps = '$adapter' | '$database' | '$trustedOrigins' | '$routes' | '$smtp' | '$custom' | '$auth';
370
371
  export type SanitizedConfigClient = Omit<Config, ServerConfigProps | 'collections' | 'areas'> & {
371
372
  collections?: BuiltCollectionClient[];
372
373
  areas?: BuiltAreaClient[];
373
374
  };
374
- export type BuiltConfigClient = Omit<BuiltConfig, ServerConfigProps | 'panel' | 'collections' | 'areas'> & {
375
+ export type BuiltConfigClient = Omit<BuiltConfig, ServerConfigProps | '$plugins' | 'panel' | 'collections' | 'areas'> & {
375
376
  collections: BuiltCollectionClient[];
376
377
  areas: BuiltAreaClient[];
377
378
  icons: Dic<Component<IconProps>>;
@@ -1,6 +1,6 @@
1
- import { RIME_DEV_CACHE_DIR } from '../../constant.server.js';
2
1
  import fs from 'fs';
3
2
  import path from 'path';
3
+ import { RIME_DEV_CACHE_DIR } from '../../constant.server.js';
4
4
  const dev = process.env.NODE_ENV === 'development';
5
5
  if (!fs.existsSync(RIME_DEV_CACHE_DIR) && dev) {
6
6
  fs.mkdirSync(RIME_DEV_CACHE_DIR);
@@ -12,6 +12,7 @@ const clearMessage = `Are you sure you want to delete all related rime files (Y/
12
12
  - ./src/lib/${OUTPUT_DIR}
13
13
  - ./src/app.generated.d.ts
14
14
  - ./src/rime.generated.d.ts
15
+ - ./src/rime.modules.generated.d.ts
15
16
  - ./src/hooks.server.ts
16
17
  - ./drizzle.config.ts
17
18
  `;
@@ -35,6 +36,7 @@ export const clear = async (args) => {
35
36
  rmSync(path.join('src', 'hooks.server.ts'), { force: true });
36
37
  rmSync(path.join('src', 'app.generated.d.ts'), { force: true });
37
38
  rmSync(path.join('src', 'rime.generated.d.ts'), { force: true });
39
+ rmSync(path.join('src', 'rime.modules.generated.d.ts'), { force: true });
38
40
  rmSync(path.join('drizzle.config.ts'), { force: true });
39
41
  return logger.info('rime cleared');
40
42
  };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Run at prepack, after `svelte-package` (operates on the compiled `dist/`, not `src/lib`) —
3
+ * makes this package's own `$rime/modules` splits consumable by anyone who installs it:
4
+ *
5
+ * 1. Builds an export-name → split index from every module.(server.)js pair under `dist/`,
6
+ * validating it as it goes (see `buildModuleIndex` below) — a name that resolves
7
+ * ambiguously has no correct rewrite target in step 2, so this has to fail loudly here,
8
+ * not silently later.
9
+ * 2. AST-rewrites every `import ... from '$rime/modules'` found anywhere under `dist/` into
10
+ * one `import ... from '$rime/modules/<pkg>/<subpath>'` per split it actually draws from —
11
+ * the bare barrel form never survives past this step.
12
+ * 3. Writes `dist/.rime-modules.json` (subpath → real file paths, read directly by a
13
+ * consumer's Vite plugin — no fs.existsSync probing, no guessing) and
14
+ * `dist/.rime-modules.d.ts` (one `declare module '$rime/modules/<pkg>/<subpath>'` per
15
+ * split, referenced by a consumer's generated types).
16
+ */
17
+ export declare const generateManifest: () => void;
@@ -0,0 +1,221 @@
1
+ import { generate } from '@babel/generator';
2
+ import * as t from '@babel/types';
3
+ import { babelParse } from 'ast-kit';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { logger } from '../../../logger/index.server.js';
7
+ import { scanModulePairs } from '../../generate/runtime/index.server.js';
8
+ import { parseExportNames } from '../../generate/runtime/parse-exports.server.js';
9
+ import { getPackageInfoByKey } from '../util/package.server.js';
10
+ /**
11
+ * Run at prepack, after `svelte-package` (operates on the compiled `dist/`, not `src/lib`) —
12
+ * makes this package's own `$rime/modules` splits consumable by anyone who installs it:
13
+ *
14
+ * 1. Builds an export-name → split index from every module.(server.)js pair under `dist/`,
15
+ * validating it as it goes (see `buildModuleIndex` below) — a name that resolves
16
+ * ambiguously has no correct rewrite target in step 2, so this has to fail loudly here,
17
+ * not silently later.
18
+ * 2. AST-rewrites every `import ... from '$rime/modules'` found anywhere under `dist/` into
19
+ * one `import ... from '$rime/modules/<pkg>/<subpath>'` per split it actually draws from —
20
+ * the bare barrel form never survives past this step.
21
+ * 3. Writes `dist/.rime-modules.json` (subpath → real file paths, read directly by a
22
+ * consumer's Vite plugin — no fs.existsSync probing, no guessing) and
23
+ * `dist/.rime-modules.d.ts` (one `declare module '$rime/modules/<pkg>/<subpath>'` per
24
+ * split, referenced by a consumer's generated types).
25
+ */
26
+ export const generateManifest = () => {
27
+ const pkgName = getPackageInfoByKey('name');
28
+ if (!pkgName) {
29
+ throw new Error("$rime generate-manifest: could not read this package's own name from package.json");
30
+ }
31
+ const distDir = path.resolve(process.cwd(), 'dist');
32
+ if (!fs.existsSync(distDir)) {
33
+ throw new Error(`$rime generate-manifest: no dist/ at ${distDir} — run this after svelte-package, not before`);
34
+ }
35
+ const pairs = scanModulePairs(distDir);
36
+ const nameOwner = buildModuleIndex(pairs);
37
+ rewriteBarrelImports(distDir, pkgName, nameOwner);
38
+ writeManifest(distDir, pkgName, pairs);
39
+ logger.info(`[✓] $rime/modules manifest generated for ${pkgName} (${pairs.size} split(s))`);
40
+ };
41
+ /**
42
+ * Export-name → split index, with the two collision scenarios from the design doc handled
43
+ * distinctly:
44
+ * - module.ts and module.server.ts of the *same* split sharing a name — not a collision, that's
45
+ * the whole point of a pair (skip, both sides collapse to one entry). The two sides don't
46
+ * need to export the *same set* of names either — a server-only helper the client build
47
+ * never needs (e.g. cache's own `toHash`) is completely normal, not a broken pair; indexed
48
+ * from the union of both sides, worth a log for visibility, never a blocking error.
49
+ * - two *different* splits exporting the same name — the actual collision. An ambiguous
50
+ * `$rime/modules` import has no correct rewrite target, so this is a hard error here, not a
51
+ * warning: it has to be caught before the rewrite runs, not after.
52
+ */
53
+ function buildModuleIndex(pairs) {
54
+ const nameOwner = new Map();
55
+ for (const [subpath, entry] of pairs) {
56
+ const serverNames = entry.server ? parseExportNames(entry.server) : [];
57
+ const clientNames = entry.client ? parseExportNames(entry.client) : [];
58
+ if (entry.server && entry.client) {
59
+ const serverSet = new Set(serverNames);
60
+ const clientSet = new Set(clientNames);
61
+ const onlyServer = serverNames.filter((name) => !clientSet.has(name));
62
+ const onlyClient = clientNames.filter((name) => !serverSet.has(name));
63
+ if (onlyServer.length || onlyClient.length) {
64
+ logger.debug(`$rime/modules: ${subpath} — module.ts and module.server.ts export different names ` +
65
+ `(server-only: ${onlyServer.join(', ') || 'none'}, client-only: ${onlyClient.join(', ') || 'none'})`);
66
+ }
67
+ }
68
+ const names = new Set([...serverNames, ...clientNames]);
69
+ for (const name of names) {
70
+ const existing = nameOwner.get(name);
71
+ if (existing && existing !== subpath) {
72
+ throw new Error(`$rime/modules: "${name}" exported by both ${existing} and ${subpath}`);
73
+ }
74
+ nameOwner.set(name, subpath);
75
+ }
76
+ }
77
+ return nameOwner;
78
+ }
79
+ /**
80
+ * `dist/.rime-modules.json` (runtime, read by a consumer's Vite plugin) and
81
+ * `dist/.rime-modules.d.ts` (types, referenced by a consumer's generated types).
82
+ *
83
+ * Both store paths *relative to `distDir`*, not absolute — confirmed in practice that an
84
+ * absolute path here is a real bug, not just a style choice: it bakes in wherever `dist/`
85
+ * happened to sit on the machine that ran `generate-manifest`, and a consumer's Vite plugin
86
+ * (`vite/index.server.ts`'s `load()`, third-party qualified form) re-joins it onto the
87
+ * *installed* package's own root (`findInstalledPackageRoot`) at read time. On a real install
88
+ * that absolute path wouldn't even exist; on the same dev machine the package was authored on
89
+ * it silently reads the author's own working-copy `dist/` instead of what's actually installed
90
+ * in the consumer's `node_modules` - stale-looking field/behavior drift with no error raised.
91
+ *
92
+ * The `.d.ts` needs a *different* relative shape than the runtime manifest, verified directly
93
+ * (a real `tsc` run — a plain filesystem path, absolute or relative, silently resolves to zero
94
+ * exports inside a `declare module` block; only a bare package-qualified specifier, resolved
95
+ * through the same `node_modules` mechanism a real import would use, actually works): every
96
+ * `export *` target is `<pkgName>/dist/<subpath-relative-to-dist>`.
97
+ */
98
+ function writeManifest(distDir, pkgName, pairs) {
99
+ const manifest = {};
100
+ const dts = [];
101
+ const relativeToDist = (target) => path.relative(distDir, target).split(path.sep).join('/');
102
+ for (const [subpath, entry] of pairs) {
103
+ manifest[subpath] = {
104
+ client: entry.client ? relativeToDist(entry.client) : undefined,
105
+ server: entry.server ? relativeToDist(entry.server) : undefined
106
+ };
107
+ const target = entry.server || entry.client;
108
+ const bareSpecifier = `${pkgName}/dist/${relativeToDist(target)}`.replace(/\.ts$/, '.js');
109
+ dts.push(`declare module '$rime/modules/${pkgName}/${subpath}' {\n export * from '${bareSpecifier}';\n}`);
110
+ }
111
+ fs.writeFileSync(path.join(distDir, '.rime-modules.json'), JSON.stringify(manifest));
112
+ fs.writeFileSync(path.join(distDir, '.rime-modules.d.ts'), dts.join('\n\n') + '\n');
113
+ }
114
+ /**
115
+ * Walks every `.js`/`.d.ts` file under `dist/`, rewriting `import ... from '$rime/modules'`
116
+ * into one `import ... from '$rime/modules/<pkg>/<subpath>'` per split its specifiers actually
117
+ * draw from — a single barrel import can (and often will) pull names from several different
118
+ * splits at once, each becoming its own import statement.
119
+ *
120
+ * Every rewritten `.d.ts` file also gets a `/// <reference path="...">` to `.rime-modules.d.ts`
121
+ * prepended, making it self-contained — verified directly (a real `tsc` run): without it, a
122
+ * consumer never discovers the ambient `declare module '$rime/modules/<pkg>/...'` declarations
123
+ * at all, *even from a file inside this same package*, since nothing imports `.rime-modules.d.ts`
124
+ * and it's outside any project's own `include`. Doing it here means the *consumer* never needs
125
+ * to know this package exists just to get its types working — no eager, app-side aggregation
126
+ * of every installed rime-dependent package required.
127
+ */
128
+ function rewriteBarrelImports(distDir, pkgName, nameOwner) {
129
+ const walk = (dir) => {
130
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
131
+ const fullPath = path.join(dir, entry.name);
132
+ if (entry.isDirectory()) {
133
+ walk(fullPath);
134
+ continue;
135
+ }
136
+ if (!entry.name.endsWith('.js') && !entry.name.endsWith('.d.ts'))
137
+ continue;
138
+ const content = fs.readFileSync(fullPath, 'utf-8');
139
+ if (!content.includes('$rime/modules'))
140
+ continue;
141
+ const ast = babelParse(content, 'ts', { sourceType: 'module', attachComment: true });
142
+ let changed = false;
143
+ const newBody = [];
144
+ for (const node of ast.body) {
145
+ // `import { x } from '$rime/modules'` (runtime .js) and the equivalent re-export
146
+ // `export { x } from '$rime/modules'` (declaration .d.ts, what tsc/svelte-package
147
+ // actually emits — a real end-to-end run caught this, not something guessable) both
148
+ // need the same treatment.
149
+ if (t.isImportDeclaration(node) && node.source.value === '$rime/modules') {
150
+ changed = true;
151
+ newBody.push(...splitBarrelImport(node, fullPath, pkgName, nameOwner));
152
+ continue;
153
+ }
154
+ if (t.isExportNamedDeclaration(node) && node.source?.value === '$rime/modules') {
155
+ changed = true;
156
+ newBody.push(...splitBarrelReExport(node, fullPath, pkgName, nameOwner));
157
+ continue;
158
+ }
159
+ if (t.isExportAllDeclaration(node) && node.source.value === '$rime/modules') {
160
+ throw new Error(`$rime/modules: ${fullPath} does 'export * from $rime/modules' — ambiguous, can't tell which split each re-exported name belongs to. Import/re-export specific names instead.`);
161
+ }
162
+ newBody.push(node);
163
+ }
164
+ if (changed) {
165
+ ast.body = newBody;
166
+ const { code } = generate(ast, { compact: false, comments: true });
167
+ fs.writeFileSync(fullPath, entry.name.endsWith('.d.ts') ? withManifestReference(code, fullPath, distDir) : code);
168
+ }
169
+ }
170
+ };
171
+ walk(distDir);
172
+ }
173
+ function withManifestReference(code, fullPath, distDir) {
174
+ const relative = path
175
+ .relative(path.dirname(fullPath), path.join(distDir, '.rime-modules.d.ts'))
176
+ .split(path.sep)
177
+ .join('/');
178
+ const referencePath = relative.startsWith('.') ? relative : `./${relative}`;
179
+ return `/// <reference path="${referencePath}" />\n${code}`;
180
+ }
181
+ /** `import { x } from '$rime/modules'` (runtime .js) — groups specifiers by which split each
182
+ * name actually belongs to (via `nameOwner`, built by `buildModuleIndex`), one import
183
+ * statement per split. */
184
+ function splitBarrelImport(node, fullPath, pkgName, nameOwner) {
185
+ const bySubpath = new Map();
186
+ for (const spec of node.specifiers) {
187
+ if (!t.isImportSpecifier(spec)) {
188
+ throw new Error(`$rime/modules: ${fullPath} imports the barrel with a default/namespace specifier — only named imports are supported`);
189
+ }
190
+ const importedName = t.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
191
+ const subpath = nameOwner.get(importedName);
192
+ if (!subpath) {
193
+ throw new Error(`$rime/modules: '${importedName}' imported in ${fullPath} but not exported by any split`);
194
+ }
195
+ if (!bySubpath.has(subpath))
196
+ bySubpath.set(subpath, []);
197
+ bySubpath.get(subpath).push(spec);
198
+ }
199
+ return Array.from(bySubpath.entries()).map(([subpath, specs]) => t.importDeclaration(specs, t.stringLiteral(`$rime/modules/${pkgName}/${subpath}`)));
200
+ }
201
+ /** `export { x } from '$rime/modules'` — what `tsc`/`svelte-package` actually emits in `.d.ts`
202
+ * output (a real end-to-end run caught this; `import`+`export` pairs aren't the only shape).
203
+ * Same grouping as `splitBarrelImport`, just keyed off `.local` (the name as it exists in the
204
+ * source module) instead of `.imported` — `ExportSpecifier` doesn't have that field. */
205
+ function splitBarrelReExport(node, fullPath, pkgName, nameOwner) {
206
+ const bySubpath = new Map();
207
+ for (const spec of node.specifiers) {
208
+ if (!t.isExportSpecifier(spec)) {
209
+ throw new Error(`$rime/modules: ${fullPath} re-exports the barrel with a default/namespace specifier — only named exports are supported`);
210
+ }
211
+ const localName = spec.local.name;
212
+ const subpath = nameOwner.get(localName);
213
+ if (!subpath) {
214
+ throw new Error(`$rime/modules: '${localName}' re-exported in ${fullPath} but not exported by any split`);
215
+ }
216
+ if (!bySubpath.has(subpath))
217
+ bySubpath.set(subpath, []);
218
+ bySubpath.get(subpath).push(spec);
219
+ }
220
+ return Array.from(bySubpath.entries()).map(([subpath, specs]) => t.exportNamedDeclaration(null, specs, t.stringLiteral(`$rime/modules/${pkgName}/${subpath}`)));
221
+ }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { logger } from '../../logger/index.server.js';
3
+ import { spawnSync } from 'node:child_process';
3
4
  import { program } from 'commander';
4
5
  import { existsSync, writeFileSync } from 'fs';
5
6
  import { envProduction } from './build/templates.js';
@@ -44,6 +45,47 @@ program
44
45
  process.exitCode = 1;
45
46
  }
46
47
  });
48
+ program
49
+ .command('package')
50
+ .description("Builds a rime plugin/field package for publish: svelte-kit sync, svelte-package, then " +
51
+ 'generate-manifest — makes any $rime/modules splits it exports consumable by anyone who ' +
52
+ 'installs it. Equivalent to chaining those three yourself.')
53
+ .action(() => {
54
+ const sync = spawnSync('./node_modules/.bin/svelte-kit', ['sync'], { stdio: 'inherit' });
55
+ if (sync.status !== 0) {
56
+ process.exitCode = sync.status ?? 1;
57
+ return;
58
+ }
59
+ const build = spawnSync('./node_modules/.bin/svelte-package', [], { stdio: 'inherit' });
60
+ if (build.status !== 0) {
61
+ process.exitCode = build.status ?? 1;
62
+ return;
63
+ }
64
+ import('./generate-manifest/index.server.js').then(({ generateManifest }) => {
65
+ try {
66
+ generateManifest();
67
+ }
68
+ catch (error) {
69
+ logger.error(error.message);
70
+ process.exitCode = 1;
71
+ }
72
+ });
73
+ });
74
+ program
75
+ .command('generate-manifest')
76
+ .description("Run after svelte-package, before publish — makes this package's own $rime/modules " +
77
+ 'splits consumable by anyone who installs it (rewrites the barrel into qualified ' +
78
+ 'imports in dist/, writes dist/.rime-modules.json + .d.ts).')
79
+ .action(async () => {
80
+ const generateManifest = await import('./generate-manifest/index.server.js').then((m) => m.generateManifest);
81
+ try {
82
+ generateManifest();
83
+ }
84
+ catch (error) {
85
+ logger.error(error.message);
86
+ process.exitCode = 1;
87
+ }
88
+ });
47
89
  program
48
90
  .command('env')
49
91
  .option('-f, --force', 'Force generation, overwrite current', false)
@@ -54,13 +54,13 @@ export const init = async ({ force, name: incomingName, skipInstall }) => {
54
54
  }
55
55
  function setConfig(name) {
56
56
  const configDirPath = path.resolve(root, 'src/lib', INPUT_DIR);
57
- const configPath = path.join(configDirPath, 'rime.config.ts');
57
+ const configPath = path.join(configDirPath, 'rime.config.server.ts');
58
58
  if (!existsSync(configPath)) {
59
59
  if (!existsSync(configDirPath)) {
60
60
  mkdirSync(configDirPath);
61
61
  }
62
62
  writeFileSync(configPath, templates.defaultConfig(name.toString()));
63
- logger.info(`[✓] Config created at ${configDirPath}/rime.config.ts`);
63
+ logger.info(`[✓] Config created at ${configDirPath}/rime.config.server.ts`);
64
64
  }
65
65
  else {
66
66
  logger.info('[✓] Config already exists (skip)');
@@ -83,7 +83,14 @@ export const init = async ({ force, name: incomingName, skipInstall }) => {
83
83
  return;
84
84
  }
85
85
  let gitignoreContent = readFileSync(gitignorePath, 'utf-8');
86
- const updates = ['\\.cache', '/logs', '/db', '\\+rime.generated'];
86
+ const updates = [
87
+ '\\.cache',
88
+ '/logs',
89
+ '/db',
90
+ '\\+rime.generated',
91
+ 'src/app.generated.d.ts',
92
+ 'src/rime.generated.d.ts'
93
+ ];
87
94
  if (!gitignoreContent.includes('# rime'))
88
95
  gitignoreContent += '\n# rime';
89
96
  for (const line of updates) {
@@ -185,7 +192,7 @@ export const init = async ({ force, name: incomingName, skipInstall }) => {
185
192
  mkdirSync(srcDir, { recursive: true });
186
193
  }
187
194
  // Create hooks.server.ts with template content
188
- writeFileSync(hooksPath, templates.hooks, 'utf-8');
195
+ writeFileSync(hooksPath, templates.hooks(), 'utf-8');
189
196
  logger.info('[✓] hooks.server.ts created');
190
197
  }
191
198
  else {
@@ -1,4 +1,4 @@
1
1
  export declare const env: () => string;
2
2
  export declare const defaultConfig: (name: string) => string;
3
3
  export declare const drizzleConfig: (name: string) => string;
4
- export declare const hooks = "import { sequence } from '@sveltejs/kit/hooks';\nimport { handlers } from 'rimecms/server';\nimport config from './lib/+rime.generated/rime.config.server.js';\n\nexport const handle = sequence(...(await handlers(config)));\n";
4
+ export declare const hooks: () => string;
@@ -1,5 +1,5 @@
1
1
  import { randomId } from '../../../../util/random.js';
2
- import { OUTPUT_DIR } from '../../constants.js';
2
+ import { configImportPaths, OUTPUT_DIR } from '../../constants.js';
3
3
  const PACKAGE = 'rimecms';
4
4
  export const env = () => `BETTER_AUTH_SECRET=${randomId(32)}
5
5
  PUBLIC_RIME_URL=http://localhost:5173
@@ -51,9 +51,11 @@ export const config: Config = {
51
51
 
52
52
  export default defineConfig(config);
53
53
  `;
54
- export const hooks = `import { sequence } from '@sveltejs/kit/hooks';
54
+ // Regenerated fresh by rime init every time (setHooks() only skips if the file already
55
+ // exists, and `rime clear`/useConfig.js delete it first) — safe to make mode-aware.
56
+ export const hooks = () => `import { sequence } from '@sveltejs/kit/hooks';
55
57
  import { handlers } from '${PACKAGE}/server';
56
- import config from './lib/${OUTPUT_DIR}/rime.config.server.js';
58
+ import config from '.${configImportPaths().server.replace('$lib', '/lib')}';
57
59
 
58
60
  export const handle = sequence(...(await handlers(config)));
59
61
  `;
@@ -9,22 +9,24 @@ const packageManagersMap = {
9
9
  npm: 'package-lock.json',
10
10
  deno: 'deno.lock'
11
11
  };
12
+ const deps = ['drizzle-orm', '@libsql/client', '@lucide/svelte', 'sharp'];
13
+ const devDeps = ['@sveltejs/adapter-node', 'drizzle-kit'];
12
14
  const packageManagerConfigs = {
13
15
  yarn: {
14
16
  command: 'echo "yarn is not supported, please use pnpm or npm" && exit 1'
15
17
  },
16
18
  pnpm: {
17
- command: 'pnpm add -D drizzle-kit && pnpm add -D @sveltejs/adapter-node && pnpm add drizzle-orm @libsql/client @lucide/svelte sharp',
19
+ command: `pnpm add -D ${devDeps.join(' ')} && pnpm add ${deps.join(' ')}`,
18
20
  preInstall: configurePnpm,
19
21
  postInstall: () => {
20
22
  execSync('pnpm rebuild');
21
23
  }
22
24
  },
23
25
  bun: {
24
- command: 'bun add -D drizzle-kit && bun add -D @sveltejs/adapter-node && bun add drizzle-orm @libsql/client @lucide/svelte sharp'
26
+ command: `bun add -D ${devDeps.join(' ')} && bun add ${deps.join(' ')}`
25
27
  },
26
28
  npm: {
27
- command: 'npm install -D drizzle-kit && npm install -D @sveltejs/adapter-node && npm install drizzle-orm @libsql/client @lucide/svelte sharp'
29
+ command: `npm install -D ${devDeps.join(' ')} && npm install ${deps.join(' ')}`
28
30
  },
29
31
  deno: {
30
32
  command: 'echo "deno is not supported, please use pnpm or npm" && exit 1'
@@ -1,5 +1,15 @@
1
1
  export declare const OUTPUT_DIR = "+rime.generated";
2
2
  export declare const INPUT_DIR = "+rime";
3
+ /** Where the panel/live route templates (common.server.ts) should import the client config
4
+ * from, and where the dev-server watcher/hot-reload should read the server config from —
5
+ * one source of truth instead of hardcoding the path at each call site. */
6
+ export declare function configImportPaths(): {
7
+ client: string;
8
+ server: string;
9
+ };
10
+ /** Same idea as configImportPaths — one source of truth for where the generated schema
11
+ * lives instead of hardcoding the path at each call site. */
12
+ export declare function schemaPath(root?: string): string;
3
13
  /**
4
14
  * True when `metaUrl` (pass the caller's own `import.meta.url` — this can't be one shared
5
15
  * value, it depends on which file is asking) is running from inside an installed `rimecms`
@@ -1,5 +1,20 @@
1
+ import path from 'node:path';
1
2
  export const OUTPUT_DIR = '+rime.generated';
2
3
  export const INPUT_DIR = '+rime';
4
+ /** Where the panel/live route templates (common.server.ts) should import the client config
5
+ * from, and where the dev-server watcher/hot-reload should read the server config from —
6
+ * one source of truth instead of hardcoding the path at each call site. */
7
+ export function configImportPaths() {
8
+ return {
9
+ client: `$lib/${OUTPUT_DIR}/rime.config.js`,
10
+ server: `$lib/${OUTPUT_DIR}/rime.config.server.js`
11
+ };
12
+ }
13
+ /** Same idea as configImportPaths — one source of truth for where the generated schema
14
+ * lives instead of hardcoding the path at each call site. */
15
+ export function schemaPath(root = process.cwd()) {
16
+ return path.resolve(root, 'src/lib', OUTPUT_DIR, 'schema.server.ts');
17
+ }
3
18
  /**
4
19
  * True when `metaUrl` (pass the caller's own `import.meta.url` — this can't be one shared
5
20
  * value, it depends on which file is asking) is running from inside an installed `rimecms`
@@ -1,8 +1,5 @@
1
1
  import { PACKAGE_NAME } from '../../../constant.server.js';
2
- import { OUTPUT_DIR } from '../../constants.js';
3
- // Trick to prevent svelte-kit to replace $lib with resolved imports path
4
- // same thing for PACKAGE_NAME
5
- const DOLLAR_LIB = '$lib';
2
+ import { configImportPaths } from '../../constants.js';
6
3
  /**
7
4
  * Main base layout
8
5
  * /+layout.server.ts
@@ -149,8 +146,8 @@ export const load = (event: ServerLoadEvent) => event.locals.routes.panel.load.r
149
146
  const panelLayout = () => `
150
147
  <script>
151
148
  import { Panel } from '${PACKAGE_NAME}/panel';
152
- import config from '${DOLLAR_LIB}/${OUTPUT_DIR}/rime.config.js';
153
-
149
+ import config from '${configImportPaths().client}';
150
+
154
151
  const { children, data } = $props();
155
152
 
156
153
  const user = $derived.by(() => {
@@ -199,7 +196,7 @@ export const load = (event: ServerLoadEvent) => event.locals.routes.panel.load.d
199
196
  const livePage = () => `
200
197
  <script>
201
198
  import { Live } from '${PACKAGE_NAME}/panel';
202
- import config from '${DOLLAR_LIB}/${OUTPUT_DIR}/rime.config.js';
199
+ import config from '${configImportPaths().client}';
203
200
 
204
201
  const { data } = $props();
205
202
  </script>
@@ -30,15 +30,36 @@ function generateRoutes(config) {
30
30
  ensureDir(rimeRoutes);
31
31
  ensureDir(panelRoute);
32
32
  ensureDir(paramsDir);
33
- // 3. Process common routes (now includes the matcher-disambiguated [slug=collection|area] routes)
33
+ // 3. Process common routes (now includes the matcher-disambiguated [slug=collection|area] routes) —
34
+ // skip the [slug=collection]/[slug=area] routes and their matchers when there are no collections/areas
35
+ // configured, rather than generating a matcher that can never match anything.
36
+ const hasCollections = (config.collections || []).length > 0;
37
+ const hasAreas = (config.areas || []).length > 0;
34
38
  for (const [pattern, files] of Object.entries(commonRoutes)) {
39
+ if (!hasCollections && pattern.includes('[slug=collection]'))
40
+ continue;
41
+ if (!hasAreas && pattern.includes('[slug=area]'))
42
+ continue;
35
43
  for (const [fileType, templateFn] of Object.entries(files)) {
36
44
  writeRouteFile(rootRoutes, pattern, fileType, templateFn());
37
45
  }
38
46
  }
39
- // 4. Write the [slug=collection]/[slug=area] param matchers
40
- fs.writeFileSync(path.join(paramsDir, 'collection.ts'), paramMatcher((config.collections || []).map((c) => c.kebab)));
41
- fs.writeFileSync(path.join(paramsDir, 'area.ts'), paramMatcher((config.areas || []).map((a) => a.kebab)));
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.
49
+ const collectionMatcherPath = path.join(paramsDir, 'collection.ts');
50
+ if (hasCollections) {
51
+ fs.writeFileSync(collectionMatcherPath, paramMatcher((config.collections || []).map((c) => c.kebab)));
52
+ }
53
+ else {
54
+ fs.rmSync(collectionMatcherPath, { force: true });
55
+ }
56
+ const areaMatcherPath = path.join(paramsDir, 'area.ts');
57
+ if (hasAreas) {
58
+ fs.writeFileSync(areaMatcherPath, paramMatcher((config.areas || []).map((a) => a.kebab)));
59
+ }
60
+ else {
61
+ fs.rmSync(areaMatcherPath, { force: true });
62
+ }
42
63
  // 5. Handle custom routes from config
43
64
  const customRoutes = config.panel?.routes;
44
65
  if (customRoutes) {