@stonecrop/nuxt 0.25.0 → 0.26.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.
package/README.md CHANGED
@@ -211,7 +211,7 @@ export default defineNuxtConfig({
211
211
  modules: ['@stonecrop/nuxt'],
212
212
 
213
213
  stonecrop: {
214
- // Point to your own page component for slug-based routing (one route per doctype)
214
+ // Point to your own page component. Doctypes that declare a `route` get a page.
215
215
  pageComponent: 'pages/StonecropPage.vue',
216
216
 
217
217
  // Or supply a custom strategy for full control
@@ -231,7 +231,7 @@ export default defineNuxtConfig({
231
231
 
232
232
  | Option | Type | Description |
233
233
  |--------|------|-------------|
234
- | `pageComponent` | `string` | Path (relative to `srcDir`) to your page component. The module registers one route per doctype at `/<slug>`, passing `schema` and `doctype` in `route.meta`. |
234
+ | `pageComponent` | `string` | Path (relative to `srcDir`) to your page component. The module registers a route for each doctype that declares one, at exactly its `route` path, passing `schema` and `doctype` in `route.meta`. |
235
235
  | `routeStrategy` | `RouteStrategyFn` | Custom function receiving all parsed doctypes; returns a `NuxtPage[]`. Takes priority over `pageComponent`. |
236
236
  | `docbuilder` | `boolean` | Enable the DocBuilder feature at `/docbuilder`. Defaults to `false`. |
237
237
  | `doctypesDir` | `string` | Override the doctypes directory path. Defaults to `doctypes/` inside `srcDir`. |
@@ -241,17 +241,25 @@ If neither `pageComponent` nor `routeStrategy` is configured the module logs a w
241
241
 
242
242
  ## Route Generation
243
243
 
244
- ### Default: slug-based routing
244
+ ### Default: routes the doctypes declare
245
245
 
246
- The module scans your `doctypes/` folder and registers one route per JSON file:
246
+ The module scans your `doctypes/` folder and registers a route for each doctype carrying a `route`
247
+ key, at exactly that path. A doctype without one gets no page.
247
248
 
248
249
  ```
249
250
  doctypes/
250
- ├── task.json → /task (if no slug field)
251
- ├── user.json → /user/:id (if slug is "user/:id")
252
- └── project.json → /project
251
+ ├── task.json { "route": "/task/:id" } → /task/:id the record
252
+ ├── tasks.json { "route": "/task" } → /task the collection
253
+ └── task-comment.json no route → no page
253
254
  ```
254
255
 
256
+ Most doctypes are not meant to be visited. A child table's rows are edited inside their parent and
257
+ a link target is reached through the record pointing at it, so neither needs a URL of its own.
258
+
259
+ `stonecrop-schema generate` writes these keys for you: an entity and its aggregate share one URL
260
+ segment, and a doctype the schema shows as rows owned by another gets none. The generated file is
261
+ yours after that — add, change or delete a `route` and regeneration leaves it alone.
262
+
255
263
  Each route's `meta` contains the parsed doctype:
256
264
 
257
265
  ```typescript
package/dist/module.d.mts CHANGED
@@ -51,9 +51,11 @@ interface ModuleOptions {
51
51
  /** Path to doctypes folder relative to the project root (defaults to 'doctypes') */
52
52
  doctypesDir?: string;
53
53
  /**
54
- * Path to the page component used for default slug-based routing.
55
- * When `routeStrategy` is not set, one route per doctype is registered
56
- * at `/<slug>` (or `/<fileName>` if no slug) using this component.
54
+ * Path to the page component every doctype route renders.
55
+ *
56
+ * When `routeStrategy` is not set, one route is registered per doctype that **declares** a
57
+ * `route`, at exactly that path — `/order` for a collection, `/order/:id` for a record. A
58
+ * doctype without one gets no page: most doctypes are not meant to be visited directly.
57
59
  *
58
60
  * The path is resolved relative to the application's `srcDir`.
59
61
  *
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stonecrop/nuxt",
3
3
  "configKey": "stonecrop",
4
- "version": "0.25.0",
4
+ "version": "0.26.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -3,6 +3,25 @@ import { readdir, readFile } from 'node:fs/promises';
3
3
  import { dirname, extname } from 'node:path';
4
4
  import { createResolver, defineNuxtModule, useLogger, resolvePath, addLayout, extendPages, addServerHandler, addComponent, addPlugin, addImportsDir } from '@nuxt/kit';
5
5
 
6
+ function declaredRoutePages(doctypes, componentPath) {
7
+ const pages = [];
8
+ const skipped = [];
9
+ for (const { fileName, data, fields } of doctypes) {
10
+ const route = data.route;
11
+ if (typeof route !== "string" || !route.startsWith("/")) {
12
+ skipped.push(fileName);
13
+ continue;
14
+ }
15
+ pages.push({
16
+ name: `stonecrop-${fileName}`,
17
+ path: route,
18
+ file: componentPath,
19
+ meta: { schema: fields, doctype: data }
20
+ });
21
+ }
22
+ return { pages, skipped };
23
+ }
24
+
6
25
  const { resolve } = createResolver(import.meta.url);
7
26
  const STONECROP_PACKAGES = [
8
27
  "@stonecrop/aform",
@@ -144,15 +163,13 @@ const module$1 = defineNuxtModule({
144
163
  generatedPages = options.routeStrategy(doctypes);
145
164
  } else if (options.pageComponent) {
146
165
  const componentPath = resolve(appDir, options.pageComponent);
147
- generatedPages = doctypes.map(({ fileName, data, fields }) => {
148
- const slug = data.slug || fileName.toLowerCase();
149
- return {
150
- name: `stonecrop-${fileName}`,
151
- path: `/${slug}`,
152
- file: componentPath,
153
- meta: { schema: fields, doctype: data }
154
- };
155
- });
166
+ const declared = declaredRoutePages(doctypes, componentPath);
167
+ generatedPages = declared.pages;
168
+ if (declared.skipped.length > 0) {
169
+ logger.info(
170
+ `No route declared, so no page registered for: ${declared.skipped.join(", ")}. Add a "route" to a doctype to give it one.`
171
+ );
172
+ }
156
173
  } else {
157
174
  logger.warn(
158
175
  "No routeStrategy or pageComponent configured \u2014 doctype routes will not be registered. Set pageComponent to a page path or provide a routeStrategy function."
@@ -129,7 +129,7 @@
129
129
  import { ATable, ARow } from "@stonecrop/atable";
130
130
  import { CANONICAL_COMPONENTS, INTROSPECTED_IDENTITY_PROPS } from "@stonecrop/schema";
131
131
  import { computed, nextTick, ref, useId } from "vue";
132
- import { updateFieldAt } from "./docbuilderFields";
132
+ import { isValueField, updateFieldAt } from "./docbuilderFields";
133
133
  const IDENTITY_PROPS = new Set(INTROSPECTED_IDENTITY_PROPS);
134
134
  const isIdentity = (key) => IDENTITY_PROPS.has(key);
135
135
  const componentListId = useId();
@@ -196,10 +196,6 @@ const props = defineProps({
196
196
  modelValue: { type: Array, required: true }
197
197
  });
198
198
  const emit = defineEmits(["update:modelValue"]);
199
- function isValueField(f) {
200
- if (typeof f.kind === "string") return f.kind === "field";
201
- return !("schema" in f) && !("columns" in f);
202
- }
203
199
  function isLocked(f) {
204
200
  return f.source === "introspected";
205
201
  }
@@ -15,6 +15,19 @@
15
15
  */
16
16
  /** A doctype field as authored on disk: known keys plus anything the builder does not model. */
17
17
  export type Field = Record<string, unknown>;
18
+ /**
19
+ * Whether an entry is a value field — the only kind the panel renders as a row. Fieldsets and
20
+ * inline tables are preserved untouched.
21
+ *
22
+ * Classified by shape through `@stonecrop/schema`'s `inferFieldKind`, never by reading `kind`.
23
+ * `kind` is Stonecrop's own discriminant — the parser synthesizes it and nothing writes it to disk
24
+ * — so the builder, which reads raw JSON and never parses a doctype, has no business consulting it.
25
+ * It used to, only because the generator and this very save path were leaking it into the files.
26
+ *
27
+ * The rule itself is imported rather than restated: getting it wrong re-types a field silently, and
28
+ * a fieldset read as a value field renders as an editable row and loses its children on save.
29
+ */
30
+ export declare function isValueField(field: Field): boolean;
18
31
  /**
19
32
  * Set `key` on `field`, or remove it when `val` is `undefined`.
20
33
  *
@@ -1,3 +1,7 @@
1
+ import { inferFieldKind } from "@stonecrop/schema";
2
+ export function isValueField(field) {
3
+ return inferFieldKind(field) === "field";
4
+ }
1
5
  export function setOrDelete(field, key, val) {
2
6
  if (val === void 0) {
3
7
  const { [key]: _omit, ...rest } = field;
@@ -1,3 +1,4 @@
1
+ import { stripFieldKind } from "@stonecrop/schema";
1
2
  export function orderKeysByReference(next, reference) {
2
3
  if (!next || !reference) return next;
3
4
  const result = {};
@@ -13,7 +14,10 @@ export function orderKeysByReference(next, reference) {
13
14
  export function mergeSavedDoctype(existing, body, requestedName) {
14
15
  const doctypeData = {
15
16
  ...existing,
16
- fields: body.fields
17
+ // `kind` is the parser's discriminant, not authored data — the builder holds fields that
18
+ // went through `normalizeFieldKind`, so it submits one on every field. Writing it back put
19
+ // a key on disk that no author typed and that `injectKind` re-derives on every read.
20
+ fields: body.fields.map(stripFieldKind)
17
21
  };
18
22
  if (body.workflow !== void 0 && body.workflow !== null) {
19
23
  const existingActions = existing.workflow?.actions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonecrop/nuxt",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Nuxt module for Stonecrop",
5
5
  "repository": {
6
6
  "type": "git",
@@ -45,18 +45,18 @@
45
45
  "pathe": "^2.0.3",
46
46
  "pinia": "^3.0.4",
47
47
  "prompts": "^2.4.2",
48
- "@stonecrop/aform": "0.25.0",
49
- "@stonecrop/atable": "0.25.0",
50
- "@stonecrop/code-editor": "0.25.0",
51
- "@stonecrop/casl-middleware": "0.25.0",
52
- "@stonecrop/desktop": "0.25.0",
53
- "@stonecrop/graphql-client": "0.25.0",
54
- "@stonecrop/graphql-middleware": "0.25.0",
55
- "@stonecrop/schema": "0.25.0",
56
- "@stonecrop/nuxt-grafserv": "0.25.0",
57
- "@stonecrop/stonecrop": "0.25.0",
58
- "@stonecrop/themes": "0.25.0",
59
- "@stonecrop/node-editor": "0.25.0"
48
+ "@stonecrop/atable": "0.26.0",
49
+ "@stonecrop/code-editor": "0.26.0",
50
+ "@stonecrop/desktop": "0.26.0",
51
+ "@stonecrop/casl-middleware": "0.26.0",
52
+ "@stonecrop/aform": "0.26.0",
53
+ "@stonecrop/graphql-client": "0.26.0",
54
+ "@stonecrop/graphql-middleware": "0.26.0",
55
+ "@stonecrop/node-editor": "0.26.0",
56
+ "@stonecrop/nuxt-grafserv": "0.26.0",
57
+ "@stonecrop/schema": "0.26.0",
58
+ "@stonecrop/stonecrop": "0.26.0",
59
+ "@stonecrop/themes": "0.26.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@eslint/js": "^10.0.1",