diff --git a/.changeset/21018-cli-generate-picklist.md b/.changeset/21018-cli-generate-picklist.md new file mode 100644 index 00000000000..24ef66b8474 --- /dev/null +++ b/.changeset/21018-cli-generate-picklist.md @@ -0,0 +1,11 @@ +--- +'@objectstack/cli': minor +--- + +feat(cli): `objectstack generate picklist NAME` scaffolds a shared option list, and the metadata summary counts picklists + +Clause-②: yes (widening) + +- **`objectstack generate picklist NAME`** (alias `os g picklist`) writes `src/picklists/NAME.picklist.ts`, a list declared with `definePicklist({ name, label, options })`, and adds its export line to `src/picklists/index.ts`. The list is collected under the `picklists` stack key. A select field takes its options from the list by naming it, `Field.select({ picklist: 'NAME' })`, in place of options of its own. The server serves that field with the list's options resolved onto it, together with any options other packages add through `picklistExtensions`, and judges writes against them. `objectstack validate` and `objectstack build` refuse a field whose `picklist` names no list the stack declares, and so does the boot. +- **`objectstack init`** wires the new `src/picklists` barrel in the `app` and `plugin` templates, the same way it wires every other directory `objectstack generate` writes into: an empty `src/picklists/index.ts` and a `picklists: exportsOf(picklists)` key in `objectstack.config.ts`. A project scaffolded by an earlier release keeps its config. `objectstack generate picklist` then reports the list as not wired and prints the import line and the `defineStack` key to add. +- **The metadata summary** that `objectstack validate`, `objectstack build` and `objectstack info` print counts the picklists a stack declares, in the `Data:` row: `Data: 1 Objects 3 Fields 1 Picklists`. A stack that declares none prints the row it printed before. The `stats` object in the `--json` output of the same three commands gains a `picklists` count. A `picklistExtensions` entry is not counted as a list. diff --git a/.changeset/21018-create-objectstack-picklists-barrel.md b/.changeset/21018-create-objectstack-picklists-barrel.md new file mode 100644 index 00000000000..2acd832195c --- /dev/null +++ b/.changeset/21018-create-objectstack-picklists-barrel.md @@ -0,0 +1,11 @@ +--- +'create-objectstack': minor +--- + +feat(create-objectstack): the blank starter wires a `src/picklists` barrel for `objectstack generate picklist` + +Clause-②: yes (widening) + +A new blank project ships an empty `src/picklists/index.ts`, and its `objectstack.config.ts` imports it and hands its exports to `defineStack` under `picklists`, as it already does for every other directory `objectstack generate` writes into. `objectstack generate picklist NAME` then writes `src/picklists/NAME.picklist.ts` and its export line, and the list is part of the stack with no edit to the config. A select field takes its options from the list with `Field.select({ picklist: 'NAME' })`, and the server serves that field with the list's options. + +A project scaffolded by an earlier release keeps its config. There, `objectstack generate picklist NAME` writes the list, reports that it does not reach the stack, and prints the import line and the `defineStack` key that wire `src/picklists`. diff --git a/content/docs/deployment/cli.mdx b/content/docs/deployment/cli.mdx index 1272938abfa..b27c47ecafb 100644 --- a/content/docs/deployment/cli.mdx +++ b/content/docs/deployment/cli.mdx @@ -1483,6 +1483,7 @@ os g flow customer # Generate a flow that runs when a Customer changes os g dashboard sales # Generate a dashboard os g app customer # Generate an app whose navigation opens Customer os g skill lead_qual # Generate an AI skill +os g picklist industry # Generate a shared option list select fields name os g object task -d lib/ # Override target directory os g object task --dry-run # Preview without writing @@ -1499,6 +1500,7 @@ os g object task --dry-run # Preview without writing | `dashboard` | `src/dashboards/` | `NAME.dashboard.ts` | `dashboards` | Analytics dashboard | | `app` | `src/apps/` | `NAME.app.ts` | `apps` | Application navigation | | `skill` | `src/skills/` | `NAME.skill.ts` | `skills` | AI skill — the ADR-0063 extension primitive | +| `picklist` | `src/picklists/` | `NAME.picklist.ts` | `picklists` | Shared option list that select fields reference by name | Every scaffold is written as `NAME.TYPE.ts`, and the infix is read from the @@ -1555,7 +1557,7 @@ the rules a JSON Schema can express and names the rest under **What it does:** 1. For a type that names an object (`object`, `view`, `action`, `flow`, `app`), reads `manifest.namespace` from the project config (`objectstack.config.ts`, `.js` or `.mjs`) and prefixes the object name with it (see above) -2. Creates the TypeScript file — an `object` declared with `ObjectSchema.create({ … })`, the same shape the `os init` templates write; a `skill` declared with `defineSkill({ … })`; the other types as typed literals (`UI.View`, `UI.Action`, `Automation.Flow`, `UI.Dashboard`, `UI.App`) +2. Creates the TypeScript file — an `object` declared with `ObjectSchema.create({ … })`, the same shape the `os init` templates write; a `skill` declared with `defineSkill({ … })`; a `picklist` declared with `definePicklist({ … })`, which a select field names with `Field.select({ picklist: 'NAME' })` in place of its own `options`, and which the server resolves into that field's `options`; the other types as typed literals (`UI.View`, `UI.Action`, `Automation.Flow`, `UI.Dashboard`, `UI.App`) 3. Creates or updates the barrel `index.ts` in the target directory 4. Shows a hint to run `objectstack validate` diff --git a/packages/cli/README.md b/packages/cli/README.md index c6eb92396c1..02c01ebe187 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -58,7 +58,11 @@ os compile | `os generate ` | Generate metadata files (alias: `os g`) | | `os create [name]` | Scaffold a standalone **kernel code** plugin project (the `Plugin` contract, built by `tsc`) from a built-in template | -Available generate types: `object`, `view`, `action`, `flow`, `dashboard`, `app`, `skill` +Available generate types: `object`, `view`, `action`, `flow`, `dashboard`, `app`, `skill`, `picklist` + +`picklist` writes a shared option list (`src/picklists/.picklist.ts`, with +`definePicklist`). A select field names it with `Field.select({ picklist: '' })` +in place of its own `options`, and the server serves that field with the list's options. `agent` is **retired** (ADR-0063 §2): agents are platform-internal, so a scaffolded `src/agents/*.ts` validated, published and was then filtered out of the runtime diff --git a/packages/cli/src/commands/generate-picklist.pin.test.ts b/packages/cli/src/commands/generate-picklist.pin.test.ts new file mode 100644 index 00000000000..8e702805ce2 --- /dev/null +++ b/packages/cli/src/commands/generate-picklist.pin.test.ts @@ -0,0 +1,127 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * PIN: `os generate picklist NAME` writes a shared option list that the + * runtime SERVES, not one it ignores. + * + * ## What was measured before the row existed + * + * On `origin/main` 58a77dbde2, `os generate picklist industry` printed + * `Unknown type: picklist` and exited 1, and the blank starter wired no + * `src/picklists` barrel. A list could be authored only by hand, and the + * command an author reaches for first did not know the kind. + * + * ## Why the row waited, and what this file holds against + * + * A scaffold of a kind with no runtime reader validates, builds, and then + * serves nothing: the list registers and no field ever carries its options. + * The row was held until the engine resolved `picklist` into `options` + * (`@objectstack/objectql`, `picklist-resolution.ts`). So the obligation here + * is end to end, not a template string: the file the generator writes, + * loaded the way `os validate` loads authored TypeScript and collected under + * the stack key `os init` and the blank starter wire its barrel into, is what + * the engine resolves onto a field that names it. + * + * Every fact about the row is READ off the roster (`GENERATOR_SCAFFOLD_TARGETS`) + * and the registry (`metadataFileName`), never restated, except the three the + * card names: the type `picklist`, the directory `src/picklists`, and the + * stack key `picklists` the engine reads. + * + * The control: the same object with no list registered serves no options, so + * a green here cannot come from a field that carried options already. + */ + +import { afterAll, describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { bundleRequire } from 'bundle-require'; +import { Field, ObjectSchema, PicklistSchema, PicklistServedFieldSchema } from '@objectstack/spec/data'; +import { ObjectQL } from '@objectstack/objectql'; +import { GENERATOR_SCAFFOLD_TARGETS } from './generate.js'; +import { metadataFileName } from '../utils/metadata-file-name.js'; +import { BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js'; + +const STEM = 'industry'; + +const PICKLIST = GENERATOR_SCAFFOLD_TARGETS.find((t) => t.type === 'picklist'); + +/** + * Inside this package's own `node_modules`, for the reason + * `test/generate-scaffold-validates.test.ts` gives: git-ignored, and the + * scaffold's `@objectstack/spec/data` import resolves from here exactly as it + * would in an author's project. + */ +const TMP_ROOT = fs.mkdtempSync( + path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'node_modules', '.generate-picklist-pin-'), +); + +afterAll(() => { + fs.rmSync(TMP_ROOT, { recursive: true, force: true }); +}); + +/** Materialize the scaffold through the loader `os validate` uses. */ +async function loadScaffold(): Promise> { + if (!PICKLIST) throw new Error('no `picklist` generator on the roster'); + const file = path.join(TMP_ROOT, metadataFileName('picklist', STEM) ?? 'unnamed.ts'); + fs.writeFileSync(file, PICKLIST.generate(STEM), 'utf8'); + const { mod } = await bundleRequire({ filepath: file, external: BUNDLE_REQUIRE_EXTERNALS }); + return ((mod as { default?: unknown }).default ?? mod) as Record; +} + +/** An object with one select field that names the list, as an author writes it. */ +const account = (listName: string) => ObjectSchema.create({ + name: 'pin_account', + label: 'Account', + sharingModel: 'private', + fields: { + industry: Field.select({ picklist: listName, label: 'Industry' }), + }, +}); + +/** Register a package the way the boot does, through the stack keys it declares. */ +function serve(collections: Record) { + const engine = new ObjectQL(); + engine.registerApp({ id: 'com.example.picklist_pin', name: 'picklist_pin', ...collections } as never); + return engine.registry.getObject('pin_account')?.fields.industry as Record | undefined; +} + +describe('`os generate picklist` is on the roster, where the starter wires it', () => { + it('scaffolds into src/picklists, collected under the `picklists` stack key', () => { + expect(PICKLIST, 'the `picklist` generator').toBeDefined(); + expect(PICKLIST!.defaultDir).toBe('src/picklists'); + expect(PICKLIST!.stackKey).toBe('picklists'); + // A list names no object, so a namespaced project writes the name as typed, + // and it needs no capability token to load. + expect(PICKLIST!.namesObject).toBe(false); + expect(PICKLIST!.requires).toEqual([]); + }); + + it('writes NAME.picklist.ts, the registry\'s own pattern for the kind', () => { + expect(metadataFileName('picklist', STEM)).toBe(`${STEM}.picklist.ts`); + }); +}); + +describe('the scaffold the generator writes is a list the engine serves', () => { + it('loads as a picklist named what `os g` reports it reached', async () => { + const list = await loadScaffold(); + expect(PicklistSchema.safeParse(list).success).toBe(true); + expect(list.name).toBe(PICKLIST!.itemName(STEM)); + }); + + it('resolves onto a select field that names it: the served field carries the list\'s options', async () => { + const list = await loadScaffold(); + const field = serve({ [PICKLIST!.stackKey]: [list], objects: [account(PICKLIST!.itemName(STEM))] }); + + expect(field?.picklist).toBe(STEM); + expect(field?.options).toEqual(list.options); + // The served contract the object read exits owe a client. + expect(PicklistServedFieldSchema.safeParse(field).success).toBe(true); + }); + + it('control: the same field with no list registered serves no options', () => { + const field = serve({ objects: [account(STEM)] }); + expect(field?.picklist).toBe(STEM); + expect(field?.options).toBeUndefined(); + }); +}); diff --git a/packages/cli/src/commands/generate.ts b/packages/cli/src/commands/generate.ts index a30c51fad6f..06805da86e8 100644 --- a/packages/cli/src/commands/generate.ts +++ b/packages/cli/src/commands/generate.ts @@ -105,8 +105,9 @@ const FLOW_SCAFFOLD_REQUIRES = ['automation', 'triggers'] as const; * flag against the templates. * * Only object names are prefixed. The scaffold's own `name` on an action, a - * flow, a dashboard, an app or a skill is not judged against the namespace by - * any gate `os validate` runs, so it stays the name the author typed. A view + * flow, a dashboard, an app, a skill or a picklist is not judged against the + * namespace by any gate `os validate` runs, so it stays the name the author + * typed. A view * container's own `name` IS an object name — the container is registered under * the object it binds to — so it is prefixed with it (#20215). * @@ -539,6 +540,59 @@ const ${toCamelCase(name)}Skill = defineSkill({ }); export default ${toCamelCase(name)}Skill; +`, + }, + + picklist: { + description: 'Shared option list that select fields reference by name', + defaultDir: 'src/picklists', + /** + * A shared option list (`data/picklist.zod.ts`): one `options` array that + * select fields on any object take their options from by NAMING the list, + * `Field.select({ picklist: 'NAME' })`, instead of each carrying a copy. + * + * Written as `NAME.picklist.ts`, the registry's own pattern for the kind, + * through {@link metadataFileName} like every other type here, with no + * override. Declared through `definePicklist`, so the list is parsed the + * moment this module loads: an unknown key or an empty `options` is a + * startup error naming it, not a list that goes missing later. + * + * Every door a referencing field passes resolves the name, so the list + * this writes is one the runtime serves, not a declaration it ignores: + * + * - `os validate` and `os build` refuse a field whose `picklist` names no + * list the stack declares (`utils/picklist-references.ts`); + * - the boot refuses the same unresolved name, and serves every field that + * names a list with the list's options resolved onto it, together with + * the options other packages add through `picklistExtensions` + * (`@objectstack/objectql`, `picklist-resolution.ts`); + * - a write to such a field is judged against that resolved set. + * + * The emitted header states the one rule an author meets next: a field + * that names the list declares no `options` of its own, because + * `FieldSchema` refuses the two together. + */ + namesObject: false, + itemName: (name: string) => toSnakeCase(name), + generate: (name: string) => `import { definePicklist } from '@objectstack/spec/data'; + +/** + * ${toTitleCase(name)} Picklist + * + * A shared option list. A select field offers these options by naming the + * list — Field.select({ picklist: '${toSnakeCase(name)}' }) — and declares no + * \`options\` of its own: a field declaring both is refused. + */ +const ${toCamelCase(name)}Picklist = definePicklist({ + name: '${toSnakeCase(name)}', + label: '${toTitleCase(name)}', + options: [ + { label: 'Option A', value: 'option_a' }, + { label: 'Option B', value: 'option_b' }, + ], +}); + +export default ${toCamelCase(name)}Picklist; `, }, }; @@ -1267,7 +1321,8 @@ async function runMetadataGeneration(type: string, name: string, flags: { dir?: // them: the scaffold file, and the barrel re-export line. They are the two // files one name reaches (#16541), and rendering them at the single point // where the name has finished being derived is what lets one refusal cover - // all 14 emission sites across all 7 generators instead of 14 patches. + // every emission site of every generator (14 across 7 when it landed) + // instead of one patch per site. const content = generator.generate(name, namespace); const exportLine = `export { default as ${toCamelCase(name)} } from '${moduleSpecifier}';`; diff --git a/packages/cli/src/utils/format.metadata-stats-package-fold.test.ts b/packages/cli/src/utils/format.metadata-stats-package-fold.test.ts index 18797367051..16f4e4f2338 100644 --- a/packages/cli/src/utils/format.metadata-stats-package-fold.test.ts +++ b/packages/cli/src/utils/format.metadata-stats-package-fold.test.ts @@ -76,6 +76,7 @@ const ZEROES: MetadataStats = { objects: 0, objectExtensions: 0, fields: 0, + picklists: 0, views: 0, pages: 0, apps: 0, diff --git a/packages/cli/src/utils/format.metadata-stats-picklists.test.ts b/packages/cli/src/utils/format.metadata-stats-picklists.test.ts new file mode 100644 index 00000000000..f80c2b59095 --- /dev/null +++ b/packages/cli/src/utils/format.metadata-stats-picklists.test.ts @@ -0,0 +1,102 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * PIN: the metadata summary `os validate`, `os build` and `os info` print + * counts the picklists a stack declares. + * + * ## What was measured before the row existed + * + * On `origin/main` 58a77dbde2, a blank starter with one picklist wired under + * `picklists` and one select field naming it printed, through `os validate`: + * + * Data: 1 Objects 3 Fields + * + * The list was declared, loaded and resolved, and the summary that answers + * "what does this stack declare" said nothing about it. With `os generate + * picklist` on the roster, an author who runs it and then `os validate` reads + * that summary to see the scaffold arrive. + * + * ## What is pinned + * + * - the READER (`collectMetadataStats`) counts `picklists`, in both ADR-0130 + * D4 shapes, through the one fold every other member goes through; + * - the PRINTER renders the count in the `Data:` row, the domain the kind + * belongs to (`kernel/metadata-plugin.zod.ts` files it under `data`); + * - a stack with no picklists prints the `Data:` row it printed before, + * because a zero item is filtered like every other one; + * - a `picklistExtensions` entry is not a list: it adds options to a list + * another package owns, so it is not counted as one. + */ + +import { describe, expect, it } from 'vitest'; +import { collectMetadataStats, printMetadataStats, type MetadataStats } from './format.js'; + +/** Drop SGR sequences so an assertion reads the words, not chalk's opinion. */ +const stripAnsi = (s: string) => s.replace(/\u001B\[[0-9;]*m/g, ''); + +function dataRow(stats: MetadataStats): string | undefined { + const captured: string[] = []; + const original = console.log; + console.log = (...args: unknown[]) => { + captured.push(args.map(String).join(' ')); + }; + try { + printMetadataStats(stats); + } finally { + console.log = original; + } + return stripAnsi(captured.join('\n')).split('\n').find((l) => l.trim().startsWith('Data:'))?.trim(); +} + +const manifest = { + id: 'com.example.pk', + name: 'pk', + version: '1.0.0', + type: 'app', + namespace: 'pk', +}; + +const INDUSTRY = { name: 'industry', label: 'Industry', options: [{ label: 'Tech', value: 'tech' }] }; +const REGION = { name: 'region', label: 'Region', options: [{ label: 'EMEA', value: 'emea' }] }; + +const ACCOUNT = { + name: 'pk_account', + label: 'Account', + sharingModel: 'private', + fields: { + name: { type: 'text', label: 'Name' }, + industry: { type: 'select', label: 'Industry', picklist: 'industry' }, + }, +}; + +describe('collectMetadataStats counts the picklists a stack declares', () => { + it('a top-level stack', () => { + expect(collectMetadataStats({ manifest, picklists: [INDUSTRY, REGION], objects: [ACCOUNT] }).picklists).toBe(2); + }); + + it('an option-B stack, every definition inside `packages[]`', () => { + const optionB = { manifest, packages: [{ manifest: { ...manifest, picklists: [INDUSTRY], objects: [ACCOUNT] } }] }; + expect(collectMetadataStats(optionB).picklists).toBe(1); + }); + + it('a `picklistExtensions` entry is not a list', () => { + const extending = { manifest, picklistExtensions: [{ extend: 'industry', options: [{ label: 'Bio', value: 'bio' }] }] }; + expect(collectMetadataStats(extending).picklists).toBe(0); + }); + + it('a stack that declares none counts zero', () => { + expect(collectMetadataStats({ manifest, objects: [ACCOUNT] }).picklists).toBe(0); + }); +}); + +describe('printMetadataStats renders the count in the Data: row', () => { + it('beside the objects and fields that name the lists', () => { + const stats = collectMetadataStats({ manifest, picklists: [INDUSTRY], objects: [ACCOUNT] }); + expect(dataRow(stats)).toBe('Data: 1 Objects 2 Fields 1 Picklists'); + }); + + it('control: a stack with no picklists prints the row it printed before', () => { + const stats = collectMetadataStats({ manifest, objects: [ACCOUNT] }); + expect(dataRow(stats)).toBe('Data: 1 Objects 2 Fields'); + }); +}); diff --git a/packages/cli/src/utils/format.ts b/packages/cli/src/utils/format.ts index c03c3f3a8f8..59eec393d5a 100644 --- a/packages/cli/src/utils/format.ts +++ b/packages/cli/src/utils/format.ts @@ -558,6 +558,7 @@ export interface MetadataStats { objects: number; objectExtensions: number; fields: number; + picklists: number; views: number; pages: number; apps: number; @@ -657,6 +658,7 @@ export function collectMetadataStats(config: any): MetadataStats { objects: count(stack.objects), objectExtensions: count(stack.objectExtensions), fields, + picklists: count(stack.picklists), views: count(stack.views), pages: count(stack.pages), apps: count(stack.apps), @@ -1483,6 +1485,7 @@ export function printMetadataStats(stats: MetadataStats) { items: [ ['Objects', stats.objects], ['Fields', stats.fields], + ['Picklists', stats.picklists], ['Extensions', stats.objectExtensions], ['Datasources', stats.datasources], ], diff --git a/packages/cli/test/print-metadata-stats-zero-row.test.ts b/packages/cli/test/print-metadata-stats-zero-row.test.ts index 905c954b9b8..5ac9e9402c4 100644 --- a/packages/cli/test/print-metadata-stats-zero-row.test.ts +++ b/packages/cli/test/print-metadata-stats-zero-row.test.ts @@ -50,6 +50,7 @@ const ZERO_APPS_STATS: MetadataStats = { objects: 1, objectExtensions: 0, fields: 2, + picklists: 0, views: 0, pages: 0, apps: 0, diff --git a/packages/create-objectstack/README.md b/packages/create-objectstack/README.md index 2e9c0f976f5..f0ad03beea3 100644 --- a/packages/create-objectstack/README.md +++ b/packages/create-objectstack/README.md @@ -85,7 +85,8 @@ my-app/ ├── flows/index.ts # already wired into objectstack.config.ts ├── dashboards/index.ts ├── apps/index.ts - └── skills/index.ts + ├── skills/index.ts + └── picklists/index.ts ``` Next steps inside the project: diff --git a/packages/create-objectstack/src/templates/blank/README.md b/packages/create-objectstack/src/templates/blank/README.md index 5830ac33994..3f248a03293 100644 --- a/packages/create-objectstack/src/templates/blank/README.md +++ b/packages/create-objectstack/src/templates/blank/README.md @@ -69,11 +69,11 @@ for OAuth, API keys, and which objects/actions become tools. the wiring for every directory below - `src/objects/` — object definitions (one file per object) - `src/views/`, `src/actions/`, `src/flows/`, `src/dashboards/`, `src/apps/`, - `src/skills/` — empty to start. Each directory's `index.ts` exports what it - holds and `objectstack.config.ts` hands those exports to the stack, so - `objectstack generate flow NAME` (or `view`, `action`, …) adds a file and one - export line, and the item is loaded, and counted by `pnpm validate`, with no - edit to the config + `src/skills/`, `src/picklists/` — empty to start. Each directory's `index.ts` + exports what it holds and `objectstack.config.ts` hands those exports to the + stack, so `objectstack generate flow NAME` (or `view`, `action`, …) adds a + file and one export line, and the item is loaded, and counted by + `pnpm validate`, with no edit to the config ## Connectors (default providers) diff --git a/packages/create-objectstack/src/templates/blank/objectstack.config.ts b/packages/create-objectstack/src/templates/blank/objectstack.config.ts index 17e29830585..6308f29eeec 100644 --- a/packages/create-objectstack/src/templates/blank/objectstack.config.ts +++ b/packages/create-objectstack/src/templates/blank/objectstack.config.ts @@ -9,6 +9,7 @@ import * as flows from './src/flows'; import * as dashboards from './src/dashboards'; import * as apps from './src/apps'; import * as skills from './src/skills'; +import * as picklists from './src/picklists'; // Every value a barrel exports, as the list a stack key takes: typed by what // the barrel exports, and an empty list while it exports nothing yet. @@ -77,4 +78,5 @@ export default defineStack({ dashboards: exportsOf(dashboards), apps: exportsOf(apps), skills: exportsOf(skills), + picklists: exportsOf(picklists), }); diff --git a/packages/create-objectstack/src/templates/blank/src/picklists/index.ts b/packages/create-objectstack/src/templates/blank/src/picklists/index.ts new file mode 100644 index 00000000000..346a371a9ee --- /dev/null +++ b/packages/create-objectstack/src/templates/blank/src/picklists/index.ts @@ -0,0 +1,5 @@ +// The picklists in this directory. `objectstack generate picklist NAME` writes one +// here and adds its export line below. objectstack.config.ts hands every +// export of this file to the stack, so a picklist exported here is part of it, +// and a picklist file this index does not export is never loaded. +export {};