Skip to content

Commit 7d21d93

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-30349-gallery-exporter
2 parents 1765a78 + 1697d5c commit 7d21d93

10 files changed

Lines changed: 621 additions & 248 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os lint`'s `naming/namespace-prefix` warns on every bare package-authored metadata name, and no longer calls the prefix optional
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Every metadata name a package authors now carries its namespace, the way object names already do: two installed packages that share a bare name such as `send_email` often resolve to only one of them, silently. `os lint` says so for every metadata type a stack can declare (flows, actions, hooks, picklists, pages, dashboards, reports, datasets, apps, email templates, webhooks, connectors, permission sets, agents, tools, skills, jobs and the rest), with the rename as the finding's `fix`:
11+
12+
```text
13+
⚠ Flow "onboard" does not carry the package namespace "crm". … rename it to "crm_onboard" …
14+
```
15+
16+
It stays a warning: nothing is refused, and `os lint --strict` is the only way it fails a run.
17+
18+
Not judged, because the name is judged elsewhere or derived from another item: objects (`defineStack` already refuses a bare object name), views (named for their object), docs (the doc lint's `docs/namespace-prefix`), object and picklist extensions, fields, translation bundles, `functions`, and an item that declares `overrides` (it must keep the base package's name). A name that is the namespace itself, starts with `sys_`, or starts with `<namespace>.` (the dotted capability shape, `billing.refund`) carries it already. A package that declares no `manifest.namespace` is not judged.
19+
20+
`os generate` writes the names it scaffolds the same way. In a project that declares a namespace, `os g flow onboard` now writes `crm_onboard_flow`, and the same goes for actions, dashboards, apps, skills, picklists, permission sets and datasets. Before, only object names got the prefix, so a fresh scaffold drew this warning on its first lint. A name that already carries the namespace is not prefixed twice.
21+
22+
The intra-package duplicate warning stays under the same rule id, for a prefixed name declared twice as well. It no longer suggests the prefixed spelling as its rename (the new finding carries that) and no longer ends with the sentence saying the prefix is an optional convention.

‎packages/cli/src/commands/generate.ts‎

Lines changed: 65 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import { validateObjectNamespacePrefix } from '@objectstack/spec/kernel';
5959
import { singularToPlural } from '@objectstack/spec/shared';
6060
import { printHeader, printSuccess, printError, printInfo, printStep, printWarning, createTimer, isReportedError, CLI_ALIAS } from '../utils/format.js';
6161
import { metadataFileName } from '../utils/metadata-file-name.js';
62-
import { readProjectNamespace, type ProjectNamespace } from '../utils/project-namespace.js';
62+
import { namespacedItemName, readProjectNamespace, type ProjectNamespace } from '../utils/project-namespace.js';
6363
import { findEmissionParseFailures } from '../utils/emitted-source-parses.js';
6464
import { findBarrelAliasRefusal } from '../utils/importable-binding.js';
6565
import {
@@ -150,15 +150,19 @@ export interface ScaffoldBinds {
150150
* In a project whose manifest declares a `namespace`, that is
151151
* `${namespace}_${name}`, which is the name `defineStack`'s namespace-prefix
152152
* gate demands. `namesObject` says which generators write one, so that
153-
* `runMetadataGeneration` reads the manifest only for those.
154-
* `generate-object-namespace-prefix.test.ts` pins both the prefix and the
155-
* flag against the templates.
153+
* `runMetadataGeneration` refuses for those when the config does not load:
154+
* with the namespace unknown, the object name it would write is one
155+
* `defineStack` may refuse. `generate-object-namespace-prefix.test.ts` pins
156+
* both the prefix and the flag against the templates.
156157
*
157-
* Only object names are prefixed. The scaffold's own `name` on an action, a
158-
* flow, a dashboard, an app, a skill or a picklist is not judged against the
159-
* namespace by any gate `os validate` runs, so it stays the name the author
160-
* typed. A view container writes no `name` at all: it is registered under
161-
* the object it binds to (#21325, see the `view` generator).
158+
* Every other metadata name carries it too (ruling C on #30798). The
159+
* scaffold's own `name` on an action, a flow, a dashboard, an app, a skill, a
160+
* picklist, a permission set or a dataset goes through {@link itemNameFor}:
161+
* `${namespace}_${name}` in a namespaced project, unless the derived name
162+
* already carries the namespace. `os lint`'s `naming/namespace-prefix` warns
163+
* on a bare one, and `generate-scaffold-validates.test.ts` holds every
164+
* scaffold to zero findings. A view container writes no `name` at all: it is
165+
* registered under the object it binds to (#21325, see the `view` generator).
162166
*
163167
* ## A binding is read off the stack, never derived from the new item's name
164168
*
@@ -418,8 +422,8 @@ export default ${toCamelCase(name)}Views;
418422
*/
419423
namesObject: true,
420424
binds: { object: 'flag', flow: 'flag' },
421-
itemName: (name: string) => toSnakeCase(name),
422-
generate: (name: string, _namespace?: string, bindings?: ScaffoldBindings) => {
425+
itemName: (name: string, namespace?: string) => itemNameFor(toSnakeCase(name), namespace),
426+
generate: (name: string, namespace?: string, bindings?: ScaffoldBindings) => {
423427
const object = requireBinding('action', bindings, 'object');
424428
const flow = requireBinding('action', bindings, 'flow');
425429
return `import * as UI from '@objectstack/spec/ui';
@@ -428,7 +432,7 @@ export default ${toCamelCase(name)}Views;
428432
* ${toTitleCase(name)} Action
429433
*/
430434
const ${toCamelCase(name)}Action: UI.Action = {
431-
name: '${toSnakeCase(name)}',
435+
name: '${itemNameFor(toSnakeCase(name), namespace)}',
432436
label: '${toTitleCase(name)}',
433437
// Runs the flow named in \`target\` against the record it is invoked on.
434438
type: 'flow',
@@ -494,9 +498,9 @@ export default ${toCamelCase(name)}Action;
494498
*/
495499
namesObject: true,
496500
binds: { object: 'flag' },
497-
itemName: (name: string) => `${toSnakeCase(name)}_flow`,
501+
itemName: (name: string, namespace?: string) => itemNameFor(`${toSnakeCase(name)}_flow`, namespace),
498502
requires: FLOW_SCAFFOLD_REQUIRES,
499-
generate: (name: string, _namespace?: string, bindings?: ScaffoldBindings) => {
503+
generate: (name: string, namespace?: string, bindings?: ScaffoldBindings) => {
500504
const object = requireBinding('flow', bindings, 'object');
501505
return `import * as Automation from '@objectstack/spec/automation';
502506
@@ -509,7 +513,7 @@ export default ${toCamelCase(name)}Action;
509513
* not load.
510514
*/
511515
const ${toCamelCase(name)}Flow: Automation.Flow = {
512-
name: '${toSnakeCase(name)}_flow',
516+
name: '${itemNameFor(`${toSnakeCase(name)}_flow`, namespace)}',
513517
label: '${toTitleCase(name)} Flow',
514518
type: 'record_change',
515519
// Armed: the trigger below fires it. Set 'obsolete' to switch it off.
@@ -550,14 +554,14 @@ export default ${toCamelCase(name)}Flow;
550554
description: 'Analytics dashboard',
551555
defaultDir: 'src/dashboards',
552556
namesObject: false,
553-
itemName: (name: string) => `${toSnakeCase(name)}_dashboard`,
554-
generate: (name: string) => `import * as UI from '@objectstack/spec/ui';
557+
itemName: (name: string, namespace?: string) => itemNameFor(`${toSnakeCase(name)}_dashboard`, namespace),
558+
generate: (name: string, namespace?: string) => `import * as UI from '@objectstack/spec/ui';
555559
556560
/**
557561
* ${toTitleCase(name)} Dashboard
558562
*/
559563
const ${toCamelCase(name)}Dashboard: UI.Dashboard = {
560-
name: '${toSnakeCase(name)}_dashboard',
564+
name: '${itemNameFor(`${toSnakeCase(name)}_dashboard`, namespace)}',
561565
label: '${toTitleCase(name)} Dashboard',
562566
widgets: [],
563567
};
@@ -588,16 +592,16 @@ export default ${toCamelCase(name)}Dashboard;
588592
*/
589593
namesObject: true,
590594
binds: { object: 'flag' },
591-
itemName: (name: string) => `${toSnakeCase(name)}_app`,
592-
generate: (name: string, _namespace?: string, bindings?: ScaffoldBindings) => {
595+
itemName: (name: string, namespace?: string) => itemNameFor(`${toSnakeCase(name)}_app`, namespace),
596+
generate: (name: string, namespace?: string, bindings?: ScaffoldBindings) => {
593597
const object = requireBinding('app', bindings, 'object');
594598
return `import * as UI from '@objectstack/spec/ui';
595599
596600
/**
597601
* ${toTitleCase(name)} App
598602
*/
599603
const ${toCamelCase(name)}App: UI.App = {
600-
name: '${toSnakeCase(name)}_app',
604+
name: '${itemNameFor(`${toSnakeCase(name)}_app`, namespace)}',
601605
label: '${toTitleCase(name)}',
602606
navigation: [
603607
{
@@ -638,8 +642,8 @@ export default ${toCamelCase(name)}App;
638642
* harness default.
639643
*/
640644
namesObject: false,
641-
itemName: (name: string) => toSnakeCase(name),
642-
generate: (name: string) => `import { defineSkill } from '@objectstack/spec/ai';
645+
itemName: (name: string, namespace?: string) => itemNameFor(toSnakeCase(name), namespace),
646+
generate: (name: string, namespace?: string) => `import { defineSkill } from '@objectstack/spec/ai';
643647
644648
/**
645649
* ${toTitleCase(name)} Skill
@@ -653,7 +657,7 @@ export default ${toCamelCase(name)}App;
653657
* a startup error naming the key, not a field that goes missing later.
654658
*/
655659
const ${toCamelCase(name)}Skill = defineSkill({
656-
name: '${toSnakeCase(name)}',
660+
name: '${itemNameFor(toSnakeCase(name), namespace)}',
657661
label: '${toTitleCase(name)}',
658662
description: 'One line on what this skill is for — the model routes on it.',
659663
@@ -682,7 +686,7 @@ const ${toCamelCase(name)}Skill = defineSkill({
682686
// it (\`ai-skill-tool-unresolved\`), and at runtime the reference is dropped
683687
// while the instructions keep promising the capability.
684688
//
685-
// tools: ['action_${toSnakeCase(name)}', 'query_records'],
689+
// tools: ['action_${itemNameFor(toSnakeCase(name), namespace)}', 'query_records'],
686690
tools: [],
687691
});
688692
@@ -720,18 +724,18 @@ export default ${toCamelCase(name)}Skill;
720724
* `FieldSchema` refuses the two together.
721725
*/
722726
namesObject: false,
723-
itemName: (name: string) => toSnakeCase(name),
724-
generate: (name: string) => `import { definePicklist } from '@objectstack/spec/data';
727+
itemName: (name: string, namespace?: string) => itemNameFor(toSnakeCase(name), namespace),
728+
generate: (name: string, namespace?: string) => `import { definePicklist } from '@objectstack/spec/data';
725729
726730
/**
727731
* ${toTitleCase(name)} Picklist
728732
*
729733
* A shared option list. A select field offers these options by naming the
730-
* list — Field.select({ picklist: '${toSnakeCase(name)}' }) — and declares no
734+
* list — Field.select({ picklist: '${itemNameFor(toSnakeCase(name), namespace)}' }) — and declares no
731735
* \`options\` of its own: a field declaring both is refused.
732736
*/
733737
const ${toCamelCase(name)}Picklist = definePicklist({
734-
name: '${toSnakeCase(name)}',
738+
name: '${itemNameFor(toSnakeCase(name), namespace)}',
735739
label: '${toTitleCase(name)}',
736740
options: [
737741
{ label: 'Option A', value: 'option_a' },
@@ -779,8 +783,8 @@ export default ${toCamelCase(name)}Picklist;
779783
*/
780784
namesObject: true,
781785
binds: { object: 'flag' },
782-
itemName: (name: string) => toSnakeCase(name),
783-
generate: (name: string, _namespace?: string, bindings?: ScaffoldBindings) => {
786+
itemName: (name: string, namespace?: string) => itemNameFor(toSnakeCase(name), namespace),
787+
generate: (name: string, namespace?: string, bindings?: ScaffoldBindings) => {
784788
const object = requireBinding('permission', bindings, 'object');
785789
return `import { definePermissionSet } from '@objectstack/spec/security';
786790
@@ -793,7 +797,7 @@ export default ${toCamelCase(name)}Picklist;
793797
* by any one of them is granted.
794798
*/
795799
const ${toCamelCase(name)}PermissionSet = definePermissionSet({
796-
name: '${toSnakeCase(name)}',
800+
name: '${itemNameFor(toSnakeCase(name), namespace)}',
797801
label: '${toTitleCase(name)}',
798802
799803
// One entry per object, keyed by its machine name. readScope is which of
@@ -841,8 +845,8 @@ export default ${toCamelCase(name)}PermissionSet;
841845
*/
842846
namesObject: true,
843847
binds: { object: 'flag' },
844-
itemName: (name: string) => toSnakeCase(name),
845-
generate: (name: string, _namespace?: string, bindings?: ScaffoldBindings) => {
848+
itemName: (name: string, namespace?: string) => itemNameFor(toSnakeCase(name), namespace),
849+
generate: (name: string, namespace?: string, bindings?: ScaffoldBindings) => {
846850
const object = requireBinding('dataset', bindings, 'object');
847851
const dimension = object.displayField
848852
? `\n { name: ${tsString(object.displayField)}, field: ${tsString(object.displayField)} },\n `
@@ -857,7 +861,7 @@ export default ${toCamelCase(name)}PermissionSet;
857861
* name; it declares no object, field or aggregate of its own.
858862
*/
859863
const ${toCamelCase(name)}Dataset = defineDataset({
860-
name: '${toSnakeCase(name)}',
864+
name: '${itemNameFor(toSnakeCase(name), namespace)}',
861865
label: '${toTitleCase(name)}',
862866
object: ${tsString(object.name)},
863867
@@ -1093,6 +1097,19 @@ function prefixedObjectName(shortName: string, namespace?: string): string {
10931097
: `${namespace}_${shortName}`;
10941098
}
10951099

1100+
/**
1101+
* The NON-object machine name a scaffold writes (ruling C on #30798): every
1102+
* metadata name a package authors carries its namespace, the way an object
1103+
* name does. `shortName` is the name as the generator derives it
1104+
* (`task_done_flow`); it gains `${namespace}_` unless it already carries the
1105+
* namespace — the predicate `os lint`'s `naming/namespace-prefix` judges with,
1106+
* so a scaffold is never written in a spelling the lint reports. With no
1107+
* namespace it is written as derived.
1108+
*/
1109+
function itemNameFor(shortName: string, namespace?: string): string {
1110+
return namespacedItemName(shortName, namespace);
1111+
}
1112+
10961113
/**
10971114
* A TypeScript single-quoted string literal for `value`. Bindings carry text
10981115
* read off the author's stack (an object's label), so it is escaped rather
@@ -1585,7 +1602,7 @@ async function runMetadataGeneration(
15851602
console.log(chalk.dim(
15861603
// #20197: "always the same string" stopped being true for an object
15871604
// name in a namespaced project, which gains the manifest's prefix.
1588-
' write lands in the file unchanged (a namespaced project only prefixes an object name).',
1605+
' write lands in the file unchanged (a namespaced project only adds its namespace prefix).',
15891606
));
15901607
console.log(chalk.dim(
15911608
// ⛔ The examples are deliberately NOT built from what the author
@@ -1603,15 +1620,17 @@ async function runMetadataGeneration(
16031620

16041621
// The project's config, loaded once before anything is written.
16051622
//
1606-
// Its `manifest.namespace` is applied only by a generator that writes an
1607-
// object machine name (#20197) — see `objectNameFor` — and only such a
1608-
// generator refuses when the config does not load. Every generator reads
1609-
// it (#20215), because whether the config loaded BEFORE this command wrote
1610-
// anything is what the reach check below needs: a config that loaded then
1611-
// and does not load once the scaffold is in place was broken by this
1612-
// command, and the write is taken back out. A generator that names no
1613-
// object still generates into a project whose config does not load, as it
1614-
// always has.
1623+
// Its `manifest.namespace` prefixes an object machine name (#20197) — see
1624+
// `objectNameFor` — and every other metadata name a scaffold writes (ruling
1625+
// C on #30798) — see `itemNameFor`. Only a generator that writes an object
1626+
// name refuses when the config does not load, because `defineStack`
1627+
// refuses an unprefixed object; a bare non-object name is a lint warning,
1628+
// so the others still generate into such a project, as they always have,
1629+
// with the name as derived. Every generator reads it (#20215), because
1630+
// whether the config loaded BEFORE this command wrote anything is what the
1631+
// reach check below needs: a config that loaded then and does not load
1632+
// once the scaffold is in place was broken by this command, and the write
1633+
// is taken back out.
16151634
//
16161635
// BELOW the charset gate, because the prefix is a derivation and the
16171636
// #16726 position puts every derivation after that gate. ABOVE the render,
@@ -1644,8 +1663,8 @@ async function runMetadataGeneration(
16441663
console.log('');
16451664
process.exit(1);
16461665
}
1647-
if (project.kind === 'loaded') namespace = project.namespace;
16481666
}
1667+
if (project.kind === 'loaded') namespace = project.namespace;
16491668

16501669
// The object name derived from what the author TYPED as this item's name:
16511670
// the `object` scaffold's own name, and a view's, which names the object

0 commit comments

Comments
 (0)