@lssm/module.contractspec-workspace 0.0.0-canary-20251217063201 → 0.0.0-canary-20251217073102

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.
@@ -1,33 +1,105 @@
1
- import{toPascalCase as e}from"./utils.js";function t(t){let d=e(t.name.split(`.`).pop()??`App`)+`AppConfig`,f=n(t),m=r(t),h=i(`dataViews`,t.dataViews),g=i(`workflows`,t.workflows),_=a(t),v=o(t),y=s(t),b=c(t),x=l(t),S=u(t),C=t.notes?` notes: '${p(t.notes)}',\n`:``;return`import type { AppBlueprintSpec } from '@lssm/lib.contracts/app-config';
1
+ import { toPascalCase } from "./utils.js";
2
2
 
3
- export const ${d}: AppBlueprintSpec = {
3
+ //#region src/templates/app-config.ts
4
+ function generateAppBlueprintSpec(data) {
5
+ const exportName = toPascalCase(data.name.split(".").pop() ?? "App") + "AppConfig";
6
+ const capabilitiesSection = buildCapabilitiesSection(data);
7
+ const featuresSection = buildFeaturesSection(data);
8
+ const dataViewsSection = buildMappingSection("dataViews", data.dataViews);
9
+ const workflowsSection = buildMappingSection("workflows", data.workflows);
10
+ const policiesSection = buildPolicySection(data);
11
+ const themeSection = buildThemeSection(data);
12
+ const telemetrySection = buildTelemetrySection(data);
13
+ const experimentsSection = buildExperimentsSection(data);
14
+ const flagsSection = buildFeatureFlagsSection(data);
15
+ const routesSection = buildRoutesSection(data);
16
+ const notesSection = data.notes ? ` notes: '${escapeString(data.notes)}',\n` : "";
17
+ return `import type { AppBlueprintSpec } from '@lssm/lib.contracts/app-config';
18
+
19
+ export const ${exportName}: AppBlueprintSpec = {
4
20
  meta: {
5
- name: '${p(t.name)}',
6
- version: ${t.version},
7
- title: '${p(t.title)}',
8
- description: '${p(t.description)}',
9
- domain: '${p(t.domain)}',
10
- owners: [${t.owners.map(e=>`'${p(e)}'`).join(`, `)}],
11
- tags: [${t.tags.map(e=>`'${p(e)}'`).join(`, `)}],
12
- stability: '${t.stability}',
13
- appId: '${p(t.appId)}',
21
+ name: '${escapeString(data.name)}',
22
+ version: ${data.version},
23
+ title: '${escapeString(data.title)}',
24
+ description: '${escapeString(data.description)}',
25
+ domain: '${escapeString(data.domain)}',
26
+ owners: [${data.owners.map((owner) => `'${escapeString(owner)}'`).join(", ")}],
27
+ tags: [${data.tags.map((tag) => `'${escapeString(tag)}'`).join(", ")}],
28
+ stability: '${data.stability}',
29
+ appId: '${escapeString(data.appId)}',
14
30
  },
15
- ${f}${m}${h}${g}${_}${v}${y}${b}${x}${S}${C}};\n`}function n(e){return e.capabilitiesEnabled.length===0&&e.capabilitiesDisabled.length===0?``:` capabilities: {\n${e.capabilitiesEnabled.length>0?` enabled: [${e.capabilitiesEnabled.map(e=>d(e)).join(`, `)}],\n`:``}${e.capabilitiesDisabled.length>0?` disabled: [${e.capabilitiesDisabled.map(e=>d(e)).join(`, `)}],\n`:``} },\n`}function r(e){return e.featureIncludes.length===0&&e.featureExcludes.length===0?``:` features: {\n${e.featureIncludes.length>0?` include: [${e.featureIncludes.map(e=>`{ key: '${p(e)}' }`).join(`, `)}],\n`:``}${e.featureExcludes.length>0?` exclude: [${e.featureExcludes.map(e=>`{ key: '${p(e)}' }`).join(`, `)}],\n`:``} },\n`}function i(e,t){return t.length===0?``:` ${e}: {\n${t.map(e=>` ${e.slot}: {
16
- name: '${p(e.name)}',
17
- ${typeof e.version==`number`?`version: ${e.version},`:``}
18
- }`).join(`,
19
- `)}\n },\n`}function a(e){return e.policyRefs.length===0?``:` policies: [\n${e.policyRefs.map(e=>` {
20
- name: '${p(e.name)}'${typeof e.version==`number`?`,\n version: ${e.version}`:``}
21
- }`).join(`,
22
- `)}\n ],\n`}function o(e){return e.theme?` theme: {\n${` primary: { name: '${p(e.theme.name)}', version: ${e.theme.version} },\n`}${e.themeFallbacks.length>0?` fallbacks: [${e.themeFallbacks.map(e=>`{ name: '${p(e.name)}', version: ${e.version} }`).join(`, `)}],\n`:``} },\n`:``}function s(e){return e.telemetry?` telemetry: {
31
+ ${capabilitiesSection}${featuresSection}${dataViewsSection}${workflowsSection}${policiesSection}${themeSection}${telemetrySection}${experimentsSection}${flagsSection}${routesSection}${notesSection}};\n`;
32
+ }
33
+ function buildCapabilitiesSection(data) {
34
+ if (data.capabilitiesEnabled.length === 0 && data.capabilitiesDisabled.length === 0) return "";
35
+ return ` capabilities: {\n${data.capabilitiesEnabled.length > 0 ? ` enabled: [${data.capabilitiesEnabled.map((key) => formatCapabilityRef(key)).join(", ")}],\n` : ""}${data.capabilitiesDisabled.length > 0 ? ` disabled: [${data.capabilitiesDisabled.map((key) => formatCapabilityRef(key)).join(", ")}],\n` : ""} },\n`;
36
+ }
37
+ function buildFeaturesSection(data) {
38
+ if (data.featureIncludes.length === 0 && data.featureExcludes.length === 0) return "";
39
+ return ` features: {\n${data.featureIncludes.length > 0 ? ` include: [${data.featureIncludes.map((key) => `{ key: '${escapeString(key)}' }`).join(", ")}],\n` : ""}${data.featureExcludes.length > 0 ? ` exclude: [${data.featureExcludes.map((key) => `{ key: '${escapeString(key)}' }`).join(", ")}],\n` : ""} },\n`;
40
+ }
41
+ function buildMappingSection(prop, mappings) {
42
+ if (mappings.length === 0) return "";
43
+ return ` ${prop}: {\n${mappings.map((mapping) => ` ${mapping.slot}: {
44
+ name: '${escapeString(mapping.name)}',
45
+ ${typeof mapping.version === "number" ? `version: ${mapping.version},` : ""}
46
+ }`).join(",\n")}\n },\n`;
47
+ }
48
+ function buildPolicySection(data) {
49
+ if (data.policyRefs.length === 0) return "";
50
+ return ` policies: [\n${data.policyRefs.map((policy) => ` {
51
+ name: '${escapeString(policy.name)}'${typeof policy.version === "number" ? `,\n version: ${policy.version}` : ""}
52
+ }`).join(",\n")}\n ],\n`;
53
+ }
54
+ function buildThemeSection(data) {
55
+ if (!data.theme) return "";
56
+ return ` theme: {\n${` primary: { name: '${escapeString(data.theme.name)}', version: ${data.theme.version} },\n`}${data.themeFallbacks.length > 0 ? ` fallbacks: [${data.themeFallbacks.map((theme) => `{ name: '${escapeString(theme.name)}', version: ${theme.version} }`).join(", ")}],\n` : ""} },\n`;
57
+ }
58
+ function buildTelemetrySection(data) {
59
+ if (!data.telemetry) return "";
60
+ return ` telemetry: {
23
61
  spec: {
24
- name: '${p(e.telemetry.name)}'${typeof e.telemetry.version==`number`?`,\n version: ${e.telemetry.version}`:``}
62
+ name: '${escapeString(data.telemetry.name)}'${typeof data.telemetry.version === "number" ? `,\n version: ${data.telemetry.version}` : ""}
25
63
  },
26
- },\n`:``}function c(e){return e.activeExperiments.length===0&&e.pausedExperiments.length===0?``:` experiments: {\n${e.activeExperiments.length>0?` active: [${e.activeExperiments.map(e=>f(e)).join(`, `)}],\n`:``}${e.pausedExperiments.length>0?` paused: [${e.pausedExperiments.map(e=>f(e)).join(`, `)}],\n`:``} },\n`}function l(e){return e.featureFlags.length===0?``:` featureFlags: [\n${e.featureFlags.map(e=>` {
27
- key: '${p(e.key)}',
28
- enabled: ${e.enabled},
29
- ${e.variant?`variant: '${p(e.variant)}',`:``}
30
- ${e.description?`description: '${p(e.description)}',`:``}
31
- }`).join(`,
32
- `)}\n ],\n`}function u(e){return e.routes.length===0?``:` routes: [\n${e.routes.map(e=>` { ${[`path: '${p(e.path)}'`,e.label?`label: '${p(e.label)}'`:null,e.dataView?`dataView: '${p(e.dataView)}'`:null,e.workflow?`workflow: '${p(e.workflow)}'`:null,e.guardName?`guard: { name: '${p(e.guardName)}'${typeof e.guardVersion==`number`?`, version: ${e.guardVersion}`:``} }`:null,e.featureFlag?`featureFlag: '${p(e.featureFlag)}'`:null,e.experimentName?`experiment: { name: '${p(e.experimentName)}'${typeof e.experimentVersion==`number`?`, version: ${e.experimentVersion}`:``} }`:null].filter(Boolean).join(`, `)} }`).join(`,
33
- `)}\n ],\n`}function d(e){return`{ key: '${p(e)}' }`}function f(e){let t=typeof e.version==`number`?`, version: ${e.version}`:``;return`{ name: '${p(e.name)}'${t} }`}function p(e){return e.replace(/\\/g,`\\\\`).replace(/'/g,`\\'`)}export{t as generateAppBlueprintSpec};
64
+ },\n`;
65
+ }
66
+ function buildExperimentsSection(data) {
67
+ if (data.activeExperiments.length === 0 && data.pausedExperiments.length === 0) return "";
68
+ return ` experiments: {\n${data.activeExperiments.length > 0 ? ` active: [${data.activeExperiments.map((exp) => formatExperimentRef(exp)).join(", ")}],\n` : ""}${data.pausedExperiments.length > 0 ? ` paused: [${data.pausedExperiments.map((exp) => formatExperimentRef(exp)).join(", ")}],\n` : ""} },\n`;
69
+ }
70
+ function buildFeatureFlagsSection(data) {
71
+ if (data.featureFlags.length === 0) return "";
72
+ return ` featureFlags: [\n${data.featureFlags.map((flag) => ` {
73
+ key: '${escapeString(flag.key)}',
74
+ enabled: ${flag.enabled},
75
+ ${flag.variant ? `variant: '${escapeString(flag.variant)}',` : ""}
76
+ ${flag.description ? `description: '${escapeString(flag.description)}',` : ""}
77
+ }`).join(",\n")}\n ],\n`;
78
+ }
79
+ function buildRoutesSection(data) {
80
+ if (data.routes.length === 0) return "";
81
+ return ` routes: [\n${data.routes.map((route) => {
82
+ return ` { ${[
83
+ `path: '${escapeString(route.path)}'`,
84
+ route.label ? `label: '${escapeString(route.label)}'` : null,
85
+ route.dataView ? `dataView: '${escapeString(route.dataView)}'` : null,
86
+ route.workflow ? `workflow: '${escapeString(route.workflow)}'` : null,
87
+ route.guardName ? `guard: { name: '${escapeString(route.guardName)}'${typeof route.guardVersion === "number" ? `, version: ${route.guardVersion}` : ""} }` : null,
88
+ route.featureFlag ? `featureFlag: '${escapeString(route.featureFlag)}'` : null,
89
+ route.experimentName ? `experiment: { name: '${escapeString(route.experimentName)}'${typeof route.experimentVersion === "number" ? `, version: ${route.experimentVersion}` : ""} }` : null
90
+ ].filter(Boolean).join(", ")} }`;
91
+ }).join(",\n")}\n ],\n`;
92
+ }
93
+ function formatCapabilityRef(key) {
94
+ return `{ key: '${escapeString(key)}' }`;
95
+ }
96
+ function formatExperimentRef(exp) {
97
+ const version = typeof exp.version === "number" ? `, version: ${exp.version}` : "";
98
+ return `{ name: '${escapeString(exp.name)}'${version} }`;
99
+ }
100
+ function escapeString(value) {
101
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
102
+ }
103
+
104
+ //#endregion
105
+ export { generateAppBlueprintSpec };
@@ -1,42 +1,49 @@
1
- import{escapeString as e,toPascalCase as t}from"./utils.js";function n(n){let i=t(n.name.split(`.`).pop()??`DataView`)+`DataView`,a=n.fields.map(t=>` {
2
- key: '${e(t.key)}',
3
- label: '${r(t.label)}',
4
- dataPath: '${e(t.dataPath)}',
5
- ${t.format?`format: '${e(t.format)}',`:``}
6
- ${t.sortable?`sortable: true,`:``}
7
- ${t.filterable?`filterable: true,`:``}
8
- }`).join(`,
9
- `),o=n.secondaryFields?.length?`secondaryFields: [${n.secondaryFields.map(t=>`'${e(t)}'`).join(`, `)}],`:``,s=n.itemOperation?`item: { name: '${e(n.itemOperation.name)}', version: ${n.itemOperation.version} },`:``;return`import type { DataViewSpec } from '@lssm/lib.contracts/data-views';
1
+ import { escapeString, toPascalCase } from "./utils.js";
10
2
 
11
- export const ${i}: DataViewSpec = {
3
+ //#region src/templates/data-view.ts
4
+ function generateDataViewSpec(data) {
5
+ const viewVarName = toPascalCase(data.name.split(".").pop() ?? "DataView") + "DataView";
6
+ const fields = data.fields.map((field) => ` {
7
+ key: '${escapeString(field.key)}',
8
+ label: '${escape(field.label)}',
9
+ dataPath: '${escapeString(field.dataPath)}',
10
+ ${field.format ? `format: '${escapeString(field.format)}',` : ""}
11
+ ${field.sortable ? "sortable: true," : ""}
12
+ ${field.filterable ? "filterable: true," : ""}
13
+ }`).join(",\n");
14
+ const secondaryFields = data.secondaryFields?.length ? `secondaryFields: [${data.secondaryFields.map((key) => `'${escapeString(key)}'`).join(", ")}],` : "";
15
+ const itemOperation = data.itemOperation ? `item: { name: '${escapeString(data.itemOperation.name)}', version: ${data.itemOperation.version} },` : "";
16
+ return `import type { DataViewSpec } from '@lssm/lib.contracts/data-views';
17
+
18
+ export const ${viewVarName}: DataViewSpec = {
12
19
  meta: {
13
- name: '${e(n.name)}',
14
- version: ${n.version},
15
- entity: '${e(n.entity)}',
16
- title: '${r(n.title)}',
17
- description: '${r(n.description||`Describe the purpose of this data view.`)}',
18
- domain: '${r(n.domain||n.entity)}',
19
- owners: [${n.owners.map(t=>`'${e(t)}'`).join(`, `)}],
20
- tags: [${n.tags.map(t=>`'${e(t)}'`).join(`, `)}],
21
- stability: '${n.stability}',
20
+ name: '${escapeString(data.name)}',
21
+ version: ${data.version},
22
+ entity: '${escapeString(data.entity)}',
23
+ title: '${escape(data.title)}',
24
+ description: '${escape(data.description || "Describe the purpose of this data view.")}',
25
+ domain: '${escape(data.domain || data.entity)}',
26
+ owners: [${data.owners.map((owner) => `'${escapeString(owner)}'`).join(", ")}],
27
+ tags: [${data.tags.map((tag) => `'${escapeString(tag)}'`).join(", ")}],
28
+ stability: '${data.stability}',
22
29
  },
23
30
  source: {
24
31
  primary: {
25
- name: '${e(n.primaryOperation.name)}',
26
- version: ${n.primaryOperation.version},
32
+ name: '${escapeString(data.primaryOperation.name)}',
33
+ version: ${data.primaryOperation.version},
27
34
  },
28
- ${s}
35
+ ${itemOperation}
29
36
  refreshEvents: [
30
37
  // { name: 'entity.updated', version: 1 },
31
38
  ],
32
39
  },
33
40
  view: {
34
- kind: '${n.kind}',
41
+ kind: '${data.kind}',
35
42
  fields: [
36
- ${a}
43
+ ${fields}
37
44
  ],
38
- ${n.primaryField?`primaryField: '${e(n.primaryField)}',`:``}
39
- ${o}
45
+ ${data.primaryField ? `primaryField: '${escapeString(data.primaryField)}',` : ""}
46
+ ${secondaryFields}
40
47
  filters: [
41
48
  // Example filter:
42
49
  // { key: 'search', label: 'Search', field: 'fullName', type: 'search' },
@@ -51,4 +58,11 @@ ${a}
51
58
  // error: { name: 'app.data.error', version: 1 },
52
59
  },
53
60
  };
54
- `}function r(e){return e.replace(/'/g,`\\'`)}export{n as generateDataViewSpec};
61
+ `;
62
+ }
63
+ function escape(value) {
64
+ return value.replace(/'/g, "\\'");
65
+ }
66
+
67
+ //#endregion
68
+ export { generateDataViewSpec };
@@ -1,24 +1,38 @@
1
- import{toPascalCase as e}from"./utils.js";function t(t){let{name:n,version:r,description:i,stability:a,owners:o,tags:s,piiFields:c}=t,l=e(n.replace(/\./g,`_`))+`V`+r,u=l+`Payload`;return`import { defineEvent } from '@lssm/lib.contracts';
1
+ import { toPascalCase } from "./utils.js";
2
+
3
+ //#region src/templates/event.ts
4
+ /**
5
+ * Generate event spec TypeScript code.
6
+ */
7
+ function generateEventSpec(data) {
8
+ const { name, version, description, stability, owners, tags, piiFields } = data;
9
+ const eventVarName = toPascalCase(name.replace(/\./g, "_")) + "V" + version;
10
+ const payloadSchemaName = eventVarName + "Payload";
11
+ return `import { defineEvent } from '@lssm/lib.contracts';
2
12
  import { ScalarTypeEnum, SchemaModel } from '@lssm/lib.schema';
3
13
 
4
14
  // TODO: Define event payload schema
5
- export const ${u} = new SchemaModel({
6
- name: '${u}',
7
- description: 'Payload for ${n}',
15
+ export const ${payloadSchemaName} = new SchemaModel({
16
+ name: '${payloadSchemaName}',
17
+ description: 'Payload for ${name}',
8
18
  fields: {
9
19
  // Add your payload fields here
10
20
  // example: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
11
21
  },
12
22
  });
13
23
 
14
- export const ${l} = defineEvent({
15
- name: '${n}',
16
- version: ${r},
17
- description: '${i}',
18
- stability: '${a}',
19
- owners: [${o.map(e=>`'${e}'`).join(`, `)}],
20
- tags: [${s.map(e=>`'${e}'`).join(`, `)}],
21
- ${c.length>0?`pii: [${c.map(e=>`'${e}'`).join(`, `)}],`:`// pii: [],`}
22
- payload: ${u},
24
+ export const ${eventVarName} = defineEvent({
25
+ name: '${name}',
26
+ version: ${version},
27
+ description: '${description}',
28
+ stability: '${stability}',
29
+ owners: [${owners.map((o) => `'${o}'`).join(", ")}],
30
+ tags: [${tags.map((t) => `'${t}'`).join(", ")}],
31
+ ${piiFields.length > 0 ? `pii: [${piiFields.map((f) => `'${f}'`).join(", ")}],` : "// pii: [],"}
32
+ payload: ${payloadSchemaName},
23
33
  });
24
- `}export{t as generateEventSpec};
34
+ `;
35
+ }
36
+
37
+ //#endregion
38
+ export { generateEventSpec };
@@ -1,62 +1,87 @@
1
- import{toPascalCase as e}from"./utils.js";function t(t){let i=e(t.name.split(`.`).pop()??`Experiment`)+`Experiment`,a=t.variants.map(e=>{let t=e.overrides?.length?` overrides: [
2
- ${e.overrides.map(e=>` {
3
- type: '${e.type}',
4
- target: '${r(e.target)}',
5
- ${typeof e.version==`number`?`version: ${e.version},`:``}
6
- }`).join(`,
7
- `)}
8
- ],`:``;return` {
9
- id: '${r(e.id)}',
10
- name: '${r(e.name)}',
11
- ${e.description?`description: '${r(e.description)}',`:``}
12
- ${typeof e.weight==`number`?`weight: ${e.weight},`:``}
13
- ${t}
14
- }`}).join(`,
15
- `),o=n(t.allocation),s=t.successMetrics?.length?` successMetrics: [
16
- ${t.successMetrics.map(e=>` {
17
- name: '${r(e.name)}',
18
- telemetryEvent: { name: '${r(e.eventName)}', version: ${e.eventVersion} },
19
- aggregation: '${e.aggregation}',
20
- ${typeof e.target==`number`?`target: ${e.target},`:``}
21
- }`).join(`,
22
- `)}
23
- ],`:``;return`import type { ExperimentSpec } from '@lssm/lib.contracts/experiments';
1
+ import { toPascalCase } from "./utils.js";
24
2
 
25
- export const ${i}: ExperimentSpec = {
3
+ //#region src/templates/experiment.ts
4
+ function generateExperimentSpec(data) {
5
+ const specVar = toPascalCase(data.name.split(".").pop() ?? "Experiment") + "Experiment";
6
+ const variants = data.variants.map((variant) => {
7
+ const overrides = variant.overrides?.length ? ` overrides: [
8
+ ${variant.overrides.map((override) => ` {
9
+ type: '${override.type}',
10
+ target: '${escapeString(override.target)}',
11
+ ${typeof override.version === "number" ? `version: ${override.version},` : ""}
12
+ }`).join(",\n")}
13
+ ],` : "";
14
+ return ` {
15
+ id: '${escapeString(variant.id)}',
16
+ name: '${escapeString(variant.name)}',
17
+ ${variant.description ? `description: '${escapeString(variant.description)}',` : ""}
18
+ ${typeof variant.weight === "number" ? `weight: ${variant.weight},` : ""}
19
+ ${overrides}
20
+ }`;
21
+ }).join(",\n");
22
+ const allocation = renderAllocation(data.allocation);
23
+ const metrics = data.successMetrics?.length ? ` successMetrics: [
24
+ ${data.successMetrics.map((metric) => ` {
25
+ name: '${escapeString(metric.name)}',
26
+ telemetryEvent: { name: '${escapeString(metric.eventName)}', version: ${metric.eventVersion} },
27
+ aggregation: '${metric.aggregation}',
28
+ ${typeof metric.target === "number" ? `target: ${metric.target},` : ""}
29
+ }`).join(",\n")}
30
+ ],` : "";
31
+ return `import type { ExperimentSpec } from '@lssm/lib.contracts/experiments';
32
+
33
+ export const ${specVar}: ExperimentSpec = {
26
34
  meta: {
27
- name: '${r(t.name)}',
28
- version: ${t.version},
29
- title: '${r(t.name)} experiment',
30
- description: '${r(t.description||`Describe the experiment goal.`)}',
31
- domain: '${r(t.domain)}',
32
- owners: [${t.owners.map(e=>`'${r(e)}'`).join(`, `)}],
33
- tags: [${t.tags.map(e=>`'${r(e)}'`).join(`, `)}],
34
- stability: '${t.stability}',
35
+ name: '${escapeString(data.name)}',
36
+ version: ${data.version},
37
+ title: '${escapeString(data.name)} experiment',
38
+ description: '${escapeString(data.description || "Describe the experiment goal.")}',
39
+ domain: '${escapeString(data.domain)}',
40
+ owners: [${data.owners.map((owner) => `'${escapeString(owner)}'`).join(", ")}],
41
+ tags: [${data.tags.map((tag) => `'${escapeString(tag)}'`).join(", ")}],
42
+ stability: '${data.stability}',
35
43
  },
36
- controlVariant: '${r(t.controlVariant)}',
44
+ controlVariant: '${escapeString(data.controlVariant)}',
37
45
  variants: [
38
- ${a}
46
+ ${variants}
39
47
  ],
40
- allocation: ${o},
41
- ${s}
48
+ allocation: ${allocation},
49
+ ${metrics}
42
50
  };
43
- `}function n(e){switch(e.type){case`random`:return`{
51
+ `;
52
+ }
53
+ function renderAllocation(allocation) {
54
+ switch (allocation.type) {
55
+ case "random": return `{
44
56
  type: 'random',
45
- ${e.salt?`salt: '${r(e.salt)}',`:``}
46
- }`;case`sticky`:return`{
57
+ ${allocation.salt ? `salt: '${escapeString(allocation.salt)}',` : ""}
58
+ }`;
59
+ case "sticky": return `{
47
60
  type: 'sticky',
48
- attribute: '${e.attribute}',
49
- ${e.salt?`salt: '${r(e.salt)}',`:``}
50
- }`;case`targeted`:return`{
61
+ attribute: '${allocation.attribute}',
62
+ ${allocation.salt ? `salt: '${escapeString(allocation.salt)}',` : ""}
63
+ }`;
64
+ case "targeted": return `{
51
65
  type: 'targeted',
52
66
  rules: [
53
- ${e.rules.map(e=>` {
54
- variantId: '${r(e.variantId)}',
55
- ${typeof e.percentage==`number`?`percentage: ${e.percentage},`:``}
56
- ${e.policy?`policy: { name: '${r(e.policy.name)}'${typeof e.policy.version==`number`?`, version: ${e.policy.version}`:``} },`:``}
57
- ${e.expression?`expression: '${r(e.expression)}',`:``}
58
- }`).join(`,
59
- `)}
67
+ ${allocation.rules.map((rule) => ` {
68
+ variantId: '${escapeString(rule.variantId)}',
69
+ ${typeof rule.percentage === "number" ? `percentage: ${rule.percentage},` : ""}
70
+ ${rule.policy ? `policy: { name: '${escapeString(rule.policy.name)}'${typeof rule.policy.version === "number" ? `, version: ${rule.policy.version}` : ""} },` : ""}
71
+ ${rule.expression ? `expression: '${escapeString(rule.expression)}',` : ""}
72
+ }`).join(",\n")}
60
73
  ],
61
- fallback: '${e.fallback??`control`}',
62
- }`;default:return i(e)}}function r(e){return e.replace(/\\/g,`\\\\`).replace(/'/g,`\\'`)}function i(e){throw Error(`Unsupported allocation type ${e}`)}export{t as generateExperimentSpec};
74
+ fallback: '${allocation.fallback ?? "control"}',
75
+ }`;
76
+ default: return renderUnsupportedAllocation(allocation);
77
+ }
78
+ }
79
+ function escapeString(value) {
80
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
81
+ }
82
+ function renderUnsupportedAllocation(allocation) {
83
+ throw new Error(`Unsupported allocation type ${allocation}`);
84
+ }
85
+
86
+ //#endregion
87
+ export { generateExperimentSpec };
@@ -1,14 +1,27 @@
1
- import{toCamelCase as e,toKebabCase as t,toPascalCase as n}from"./utils.js";function r(r,i){let a=e(r.split(`.`).pop()??`unknown`)+`Handler`,o=n(r.split(`.`).pop()??`Unknown`)+`Spec`;return`import type { ContractHandler } from '@lssm/lib.contracts';
2
- import { ${o} } from '../contracts/${t(r)}.contracts';
1
+ import { toCamelCase, toKebabCase, toPascalCase } from "./utils.js";
3
2
 
3
+ //#region src/templates/handler.ts
4
4
  /**
5
- * Handler for ${r}
5
+ * Handler and component template generation.
6
+ * Extracted from cli-contracts/src/templates/handler.template.ts
7
+ */
8
+ /**
9
+ * Generate handler implementation template.
10
+ */
11
+ function generateHandlerTemplate(specName, kind) {
12
+ const handlerName = toCamelCase(specName.split(".").pop() ?? "unknown") + "Handler";
13
+ const specVarName = toPascalCase(specName.split(".").pop() ?? "Unknown") + "Spec";
14
+ return `import type { ContractHandler } from '@lssm/lib.contracts';
15
+ import { ${specVarName} } from '../contracts/${toKebabCase(specName)}.contracts';
16
+
17
+ /**
18
+ * Handler for ${specName}
6
19
  */
7
- export const ${a}: ContractHandler<typeof ${o}> = async (
20
+ export const ${handlerName}: ContractHandler<typeof ${specVarName}> = async (
8
21
  input,
9
22
  context
10
23
  ) => {
11
- // TODO: Implement ${i} logic
24
+ // TODO: Implement ${kind} logic
12
25
 
13
26
  try {
14
27
  // 1. Validate prerequisites
@@ -24,28 +37,43 @@ export const ${a}: ContractHandler<typeof ${o}> = async (
24
37
  throw error;
25
38
  }
26
39
  };
27
- `}function i(e,t){let r=n(e);return`import React from 'react';
40
+ `;
41
+ }
42
+ /**
43
+ * Generate React component template.
44
+ */
45
+ function generateComponentTemplate(componentName, description) {
46
+ const pascalName = toPascalCase(componentName);
47
+ return `import React from 'react';
28
48
 
29
- interface ${r}Props {
49
+ interface ${pascalName}Props {
30
50
  // TODO: Define props based on presentation spec
31
51
  }
32
52
 
33
53
  /**
34
- * ${t}
54
+ * ${description}
35
55
  */
36
- export const ${r}: React.FC<${r}Props> = (props) => {
56
+ export const ${pascalName}: React.FC<${pascalName}Props> = (props) => {
37
57
  return (
38
58
  <div>
39
59
  {/* TODO: Implement component UI */}
40
- <p>Component: ${r}</p>
60
+ <p>Component: ${pascalName}</p>
41
61
  </div>
42
62
  );
43
63
  };
44
- `}function a(e,r){let i=r===`handler`?`../handlers`:`../components`,a=n(e);return`import { describe, it, expect } from 'bun:test';
45
- import { ${a} } from '${i}/${t(e)}';
64
+ `;
65
+ }
66
+ /**
67
+ * Generate test template.
68
+ */
69
+ function generateTestTemplate(targetName, type) {
70
+ const importPath = type === "handler" ? "../handlers" : "../components";
71
+ const testName = toPascalCase(targetName);
72
+ return `import { describe, it, expect } from 'bun:test';
73
+ import { ${testName} } from '${importPath}/${toKebabCase(targetName)}';
46
74
 
47
- describe('${a}', () => {
48
- it('should ${r===`handler`?`handle valid input`:`render correctly`}', async () => {
75
+ describe('${testName}', () => {
76
+ it('should ${type === "handler" ? "handle valid input" : "render correctly"}', async () => {
49
77
  // TODO: Implement test
50
78
  expect(true).toBe(true);
51
79
  });
@@ -54,10 +82,14 @@ describe('${a}', () => {
54
82
  // TODO: Test edge cases
55
83
  });
56
84
 
57
- ${r===`handler`?`it('should handle errors appropriately', async () => {
85
+ ${type === "handler" ? `it('should handle errors appropriately', async () => {
58
86
  // TODO: Test error scenarios
59
- });`:`it('should be accessible', async () => {
87
+ });` : `it('should be accessible', async () => {
60
88
  // TODO: Test accessibility
61
89
  });`}
62
90
  });
63
- `}export{i as generateComponentTemplate,r as generateHandlerTemplate,a as generateTestTemplate};
91
+ `;
92
+ }
93
+
94
+ //#endregion
95
+ export { generateComponentTemplate, generateHandlerTemplate, generateTestTemplate };