@builder-builder/builder 0.0.14 → 0.0.16

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 (85) hide show
  1. package/dist/bb.d.ts +9 -5
  2. package/dist/bb.js +12 -5
  3. package/dist/check.d.ts +2 -2
  4. package/dist/entities/builder/builder.d.ts +19 -5
  5. package/dist/entities/builder/builder.js +24 -5
  6. package/dist/entities/builder/index.d.ts +1 -2
  7. package/dist/entities/builder/index.js +1 -2
  8. package/dist/entities/collection/collection.d.ts +7 -1
  9. package/dist/entities/collection/collection.js +9 -2
  10. package/dist/entities/collection/config.d.ts +6 -1
  11. package/dist/entities/collection/config.js +9 -2
  12. package/dist/entities/component/component.d.ts +41 -1
  13. package/dist/entities/component/component.js +9 -2
  14. package/dist/entities/component/details.d.ts +11 -2
  15. package/dist/entities/component/details.js +9 -2
  16. package/dist/entities/component/field.d.ts +10 -1
  17. package/dist/entities/component/field.js +13 -3
  18. package/dist/entities/index.d.ts +5 -1
  19. package/dist/entities/index.js +2 -0
  20. package/dist/entities/kind.d.ts +1 -1
  21. package/dist/entities/model/methods.d.ts +3 -3
  22. package/dist/entities/model/model.d.ts +5 -1
  23. package/dist/entities/model/model.js +10 -3
  24. package/dist/entities/model/models.js +9 -5
  25. package/dist/entities/option/option.d.ts +41 -1
  26. package/dist/entities/option/option.js +9 -2
  27. package/dist/entities/option/select.d.ts +7 -1
  28. package/dist/entities/option/select.js +17 -6
  29. package/dist/entities/option/toggle.d.ts +7 -1
  30. package/dist/entities/option/toggle.js +16 -4
  31. package/dist/entities/option/values.d.ts +6 -0
  32. package/dist/entities/pricing/expression.d.ts +70 -0
  33. package/dist/entities/pricing/expression.js +43 -0
  34. package/dist/entities/pricing/index.d.ts +6 -0
  35. package/dist/entities/pricing/index.js +3 -0
  36. package/dist/entities/pricing/pricing.d.ts +17 -0
  37. package/dist/entities/pricing/pricing.js +21 -0
  38. package/dist/entities/pricing/rates.d.ts +3 -0
  39. package/dist/entities/pricing/rates.js +3 -0
  40. package/dist/entities/serialise.d.ts +549 -495
  41. package/dist/entities/serialise.js +65 -41
  42. package/dist/entities/tags.d.ts +3 -0
  43. package/dist/entities/tags.js +2 -0
  44. package/dist/entities/ui/describe.d.ts +10 -1
  45. package/dist/entities/ui/describe.js +9 -2
  46. package/dist/entities/ui/input.d.ts +10 -1
  47. package/dist/entities/ui/input.js +17 -5
  48. package/dist/entities/ui/page.d.ts +10 -1
  49. package/dist/entities/ui/page.js +9 -2
  50. package/dist/entities/ui/pages.d.ts +5 -1
  51. package/dist/entities/ui/pages.js +9 -2
  52. package/dist/entities/ui/ui.d.ts +6 -1
  53. package/dist/entities/ui/ui.js +27 -12
  54. package/dist/entities/validated.d.ts +3 -1
  55. package/dist/environment.d.ts +1 -1
  56. package/dist/exception.d.ts +2 -2
  57. package/dist/index.d.ts +3 -3
  58. package/dist/index.js +1 -1
  59. package/dist/instance.d.ts +9 -0
  60. package/dist/instance.js +3 -1
  61. package/dist/mappers/index.d.ts +3 -2
  62. package/dist/mappers/index.js +2 -1
  63. package/dist/mappers/price.d.ts +3 -0
  64. package/dist/mappers/price.js +111 -0
  65. package/dist/validate/builder.js +16 -7
  66. package/dist/validate/errors.d.ts +19 -1
  67. package/dist/validate/errors.js +15 -0
  68. package/dist/validate/index.d.ts +3 -1
  69. package/dist/validate/index.js +1 -0
  70. package/dist/validate/model.js +5 -62
  71. package/dist/validate/paths.d.ts +5 -0
  72. package/dist/validate/paths.js +68 -0
  73. package/dist/validate/pricing.d.ts +8 -0
  74. package/dist/validate/pricing.js +127 -0
  75. package/dist/validate/ui.js +60 -1
  76. package/dist/validate/variants.js +4 -0
  77. package/package.json +1 -10
  78. package/dist/cli.d.ts +0 -2
  79. package/dist/cli.js +0 -53
  80. package/dist/codegen/index.d.ts +0 -7
  81. package/dist/codegen/index.js +0 -212
  82. package/dist/codegen/template.d.ts +0 -5
  83. package/dist/codegen/template.js +0 -17
  84. package/dist/entities/builder/factory.d.ts +0 -7
  85. package/dist/entities/builder/factory.js +0 -4
@@ -0,0 +1,127 @@
1
+ import * as v from 'valibot';
2
+ import { check } from '../check.js';
3
+ import { BuilderPricingExpressionSchema, BuilderPricingSerialisedSchema, BuilderRatesSchema, pricing, serialise } from '../entities/index.js';
4
+ import { validate } from './brand.js';
5
+ import { BuilderValidateErrors } from './errors.js';
6
+ import { resolver } from './resolve.js';
7
+ const EMPTY_PRICING = validate(serialise.pricing(pricing()));
8
+ const NumberSchema = v.number();
9
+ export function validatePricing(input, references = [], errors = new BuilderValidateErrors()) {
10
+ if (!check.is(BuilderPricingSerialisedSchema, input)) {
11
+ errors.invalidInput('pricing');
12
+ return [EMPTY_PRICING, errors.errors];
13
+ }
14
+ const resolve = resolver(errors, references);
15
+ const structure = validatePricingStructure(input, resolve, errors);
16
+ return [validate(structure), errors.errors];
17
+ }
18
+ export function validatePricingStructure(input, resolve, errors) {
19
+ const rates = input.rates.flatMap((entry) => {
20
+ const resolved = resolve(entry);
21
+ return v.is(BuilderRatesSchema, resolved) ? [resolved] : [];
22
+ });
23
+ const formula = input.formula == null ? null : resolveExpression(input.formula);
24
+ if (formula != null) {
25
+ walkExpression(formula, false);
26
+ }
27
+ return { rates, formula };
28
+ function resolveExpression(input) {
29
+ const resolved = resolve(input);
30
+ if (!check.is(BuilderPricingExpressionSchema, resolved)) {
31
+ return null;
32
+ }
33
+ if (check.is(NumberSchema, resolved)) {
34
+ return resolved;
35
+ }
36
+ if (resolved.kind === 'variantPrice' || resolved.kind === 'lookup') {
37
+ return resolved;
38
+ }
39
+ if (resolved.kind === 'variants') {
40
+ const inner = errors.scope('expression', () => resolveExpression(resolved.expression));
41
+ return { ...resolved, expression: inner ?? resolved.expression };
42
+ }
43
+ const left = errors.scope('left', () => resolveExpression(resolved.left));
44
+ const right = errors.scope('right', () => resolveExpression(resolved.right));
45
+ return { ...resolved, left: left ?? resolved.left, right: right ?? resolved.right };
46
+ }
47
+ function walkExpression(expression, insideVariants) {
48
+ if (check.is(NumberSchema, expression)) {
49
+ return;
50
+ }
51
+ switch (expression.kind) {
52
+ case 'variantPrice':
53
+ if (!insideVariants) {
54
+ errors.invalidPricing('scope');
55
+ }
56
+ return;
57
+ case 'lookup':
58
+ errors.scope('lookup', () => {
59
+ const exists = rates.some((entry) => entry[expression.rate] != null);
60
+ if (!exists) {
61
+ errors.missingRate(expression.rate);
62
+ }
63
+ if (!insideVariants) {
64
+ errors.invalidPricing('scope');
65
+ }
66
+ });
67
+ return;
68
+ case 'variants':
69
+ errors.scope('variants', () => {
70
+ if (insideVariants) {
71
+ errors.invalidPricing('nested-variants');
72
+ }
73
+ const inner = expression.expression;
74
+ check.assert(BuilderPricingExpressionSchema, inner);
75
+ errors.scope('expression', () => walkExpression(inner, true));
76
+ });
77
+ return;
78
+ case 'add':
79
+ case 'sub':
80
+ case 'mul':
81
+ case 'div': {
82
+ const { left, right } = expression;
83
+ check.assert(BuilderPricingExpressionSchema, left);
84
+ check.assert(BuilderPricingExpressionSchema, right);
85
+ errors.scope('left', () => walkExpression(left, insideVariants));
86
+ errors.scope('right', () => walkExpression(right, insideVariants));
87
+ return;
88
+ }
89
+ }
90
+ }
91
+ }
92
+ export function checkPricingExpectations(model, pricing, errors) {
93
+ if (pricing.formula == null) {
94
+ return;
95
+ }
96
+ const optionNames = new Set(model.options.map((option) => option.name));
97
+ walkExpression(pricing.formula);
98
+ function walkExpression(expression) {
99
+ if (!check.is(BuilderPricingExpressionSchema, expression)) {
100
+ return;
101
+ }
102
+ if (check.is(NumberSchema, expression)) {
103
+ return;
104
+ }
105
+ switch (expression.kind) {
106
+ case 'lookup':
107
+ errors.scope('lookup', () => {
108
+ if (expression.key.kind === 'option' && !optionNames.has(expression.key.name)) {
109
+ errors.unmetExpectation('option', expression.key.name);
110
+ }
111
+ });
112
+ return;
113
+ case 'variants':
114
+ errors.scope('variants', () => errors.scope('expression', () => walkExpression(expression.expression)));
115
+ return;
116
+ case 'add':
117
+ case 'sub':
118
+ case 'mul':
119
+ case 'div':
120
+ errors.scope('left', () => walkExpression(expression.left));
121
+ errors.scope('right', () => walkExpression(expression.right));
122
+ return;
123
+ default:
124
+ return;
125
+ }
126
+ }
127
+ }
@@ -1,8 +1,9 @@
1
1
  import { check } from '../check.js';
2
- import { BuilderUIInputMetadataSchema, BuilderUISerialisedSchema, serialise, uis } from '../entities/index.js';
2
+ import { BuilderModelSerialisedSchema, BuilderUIInputMetadataSchema, BuilderUISerialisedSchema, serialise, uis } from '../entities/index.js';
3
3
  import { validate } from './brand.js';
4
4
  import { BuilderValidateErrors } from './errors.js';
5
5
  import { checkExpectations } from './expectations.js';
6
+ import { checkPath, collectionConfigs } from './paths.js';
6
7
  import { resolver } from './resolve.js';
7
8
  export function validateUI(input, references = [], errors = new BuilderValidateErrors()) {
8
9
  if (!check.is(BuilderUISerialisedSchema, input)) {
@@ -82,9 +83,11 @@ export function validateUIStructure(ui, resolve, errors) {
82
83
  }
83
84
  }
84
85
  export function checkUIExpectations(mergedModel, ui, errors) {
86
+ const expectedOptionNames = collectExpectedOptionNames(ui);
85
87
  errors.scope('expectations', () => {
86
88
  checkExpectations(mergedModel, ui.expectations, errors);
87
89
  });
90
+ errors.scope('items', () => walkItems(mergedModel, ui.items));
88
91
  errors.scope('uis', () => {
89
92
  ui.uis.forEach((nested, nestedIndex) => {
90
93
  errors.scope(nestedIndex, () => {
@@ -92,4 +95,60 @@ export function checkUIExpectations(mergedModel, ui, errors) {
92
95
  });
93
96
  });
94
97
  });
98
+ function walkItems(model, items) {
99
+ items.forEach((entry, itemIndex) => {
100
+ const item = entry;
101
+ errors.scope(itemIndex, () => {
102
+ if (item.type === 'pages') {
103
+ walkPages(model, item);
104
+ return;
105
+ }
106
+ const inputs = item.inputs;
107
+ if (!Array.isArray(inputs)) {
108
+ return;
109
+ }
110
+ errors.scope('inputs', () => inputs.forEach((input, entryIndex) => {
111
+ errors.scope(entryIndex, () => walkInput(model, input));
112
+ }));
113
+ });
114
+ });
115
+ }
116
+ function walkPages(outerModel, pages) {
117
+ const collection = outerModel.collections.find((entry) => entry.name === pages.name);
118
+ if (collection == null) {
119
+ return;
120
+ }
121
+ const innerModels = collectionConfigs(collection).flatMap(({ model: configModel }) => check.is(BuilderModelSerialisedSchema, configModel) ? [configModel] : []);
122
+ if (innerModels.length === 0) {
123
+ return;
124
+ }
125
+ const [firstInnerModel] = innerModels;
126
+ errors.scope('items', () => walkItems(firstInnerModel, pages.items));
127
+ }
128
+ function walkInput(model, input) {
129
+ const path = input.path;
130
+ if (!Array.isArray(path)) {
131
+ return;
132
+ }
133
+ const leaf = path.at(-1);
134
+ if (typeof leaf === 'string' && expectedOptionNames.has(leaf)) {
135
+ return;
136
+ }
137
+ errors.scope('path', () => {
138
+ checkPath(model, path, errors);
139
+ });
140
+ }
141
+ }
142
+ function collectExpectedOptionNames(ui) {
143
+ const names = new Set();
144
+ collectFromUI(ui);
145
+ return names;
146
+ function collectFromUI(node) {
147
+ node.expectations.forEach((expectation) => {
148
+ if (expectation.kind === 'option') {
149
+ names.add(expectation.name);
150
+ }
151
+ });
152
+ node.uis.forEach((nested) => collectFromUI(nested));
153
+ }
95
154
  }
@@ -11,6 +11,10 @@ export function validateVariants(model, input, options = {}, errors = new Builde
11
11
  }
12
12
  const variants = input;
13
13
  const expected = createVariants(model);
14
+ if (Object.keys(expected).length === 0) {
15
+ errors.noComponents();
16
+ return [validate({}), errors.errors];
17
+ }
14
18
  errors.scope('variants', () => {
15
19
  checkVariants();
16
20
  checkComponents();
package/package.json CHANGED
@@ -1,23 +1,16 @@
1
1
  {
2
2
  "name": "@builder-builder/builder",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
8
8
  "default": "./dist/index.js"
9
- },
10
- "./codegen": {
11
- "types": "./dist/codegen/index.d.ts",
12
- "default": "./dist/codegen/index.js"
13
9
  }
14
10
  },
15
11
  "files": [
16
12
  "dist"
17
13
  ],
18
- "bin": {
19
- "bb": "./dist/cli.js"
20
- },
21
14
  "svelte": "./dist/index.js",
22
15
  "types": "./dist/index.d.ts",
23
16
  "publishConfig": {
@@ -76,8 +69,6 @@
76
69
  "dependencies": {
77
70
  "@floating-ui/dom": "^1.7.2",
78
71
  "@lucide/svelte": "^0.525.0",
79
- "@phenomnomnominal/tsquery": "^6.2.0",
80
- "@phenomnomnominal/tstemplate": "^0.1.0",
81
72
  "@supabase/supabase-js": "^2.104.0",
82
73
  "@upstash/ratelimit": "^2.0.5",
83
74
  "@upstash/redis": "^1.35.0",
package/dist/cli.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/cli.js DELETED
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env node
2
- import { writeFileSync } from 'node:fs';
3
- import { parseArgs } from 'node:util';
4
- import { generateOrderType } from './codegen/index.js';
5
- const DEFAULT_API_URL = 'https://builder-builder.com';
6
- run().catch((caught) => {
7
- const message = caught instanceof Error ? caught.message : String(caught);
8
- process.stderr.write(`bb: ${message}\n`);
9
- process.exit(1);
10
- });
11
- async function run() {
12
- const { values } = parseArgs({
13
- options: {
14
- id: { type: 'string' },
15
- out: { type: 'string' },
16
- api: { type: 'string', default: DEFAULT_API_URL },
17
- key: { type: 'string' }
18
- },
19
- strict: true
20
- });
21
- const id = values.id;
22
- const out = values.out;
23
- if (id == null || out == null) {
24
- process.stderr.write('Usage: bb --id <entity-id> --out <file> [--api <url>] [--key <api-key>]\n');
25
- process.exit(1);
26
- return;
27
- }
28
- const apiUrl = values.api ?? DEFAULT_API_URL;
29
- const apiKey = values.key ?? process.env.BB_API_KEY;
30
- if (apiKey == null || apiKey.length === 0) {
31
- throw new Error('missing API key — pass --key or set BB_API_KEY');
32
- }
33
- const entity = await fetchEntity({ apiUrl, id, apiKey });
34
- const generated = generateOrderType({ name: entity.name, builder: entity.builder });
35
- const fetchedAt = new Date().toISOString();
36
- const header = `// AUTO-GENERATED — DO NOT EDIT\n` +
37
- `// Source: ${id} (${entity.name}) — fetched ${fetchedAt}\n` +
38
- `// Regenerate: bb --id ${id} --out ${out}\n\n`;
39
- writeFileSync(out, header + generated);
40
- process.stdout.write(`bb: wrote ${out}\n`);
41
- }
42
- async function fetchEntity(options) {
43
- const url = `${options.apiUrl.replace(/\/$/, '')}/api/builder/${options.id}`;
44
- const response = await fetch(url, {
45
- headers: { 'X-Builder-Builder-Key': options.apiKey }
46
- });
47
- if (!response.ok) {
48
- const body = await response.text();
49
- throw new Error(`fetch ${url} failed: ${response.status} ${response.statusText} — ${body}`);
50
- }
51
- const payload = (await response.json());
52
- return payload;
53
- }
@@ -1,7 +0,0 @@
1
- import type { BuilderRefEntities, BuilderSerialised } from '../entities/index.js';
2
- export type GenerateOrderTypeInput = {
3
- readonly name: string;
4
- readonly builder: BuilderSerialised;
5
- readonly references?: BuilderRefEntities;
6
- };
7
- export declare function generateOrderType(input: GenerateOrderTypeInput): string;
@@ -1,212 +0,0 @@
1
- import ts from 'typescript';
2
- import * as v from 'valibot';
3
- import { check } from '../check.js';
4
- import { BuilderException } from '../exception.js';
5
- import { optionGraph, variantsFor } from '../mappers/index.js';
6
- import { validateBuilder } from '../validate/index.js';
7
- import { tsmember, tstype } from './template.js';
8
- export function generateOrderType(input) {
9
- const [validated, errors] = validateBuilder(input.builder, input.references ?? []);
10
- if (errors.length > 0) {
11
- throw new BuilderException(errors);
12
- }
13
- const typeName = `${pascalCase(input.name)}Order`;
14
- const typeNode = renderModelOrder(validated.model);
15
- const declaration = ts.factory.createTypeAliasDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], typeName, undefined, typeNode);
16
- const sourceFile = ts.createSourceFile('generated.ts', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
17
- const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
18
- return `${printer.printNode(ts.EmitHint.Unspecified, declaration, sourceFile)}\n`;
19
- }
20
- function renderModelOrder(model) {
21
- const graph = optionGraph(model);
22
- const componentMembers = model.components.map((component) => {
23
- return renderComponentMember(component, model, graph);
24
- });
25
- const collectionMembers = model.collections.map((collection) => {
26
- return renderCollectionMember(collection);
27
- });
28
- return ts.factory.createTypeLiteralNode([...componentMembers, ...collectionMembers]);
29
- }
30
- function renderComponentMember(component, model, graph) {
31
- const variant = renderComponentVariant(component, model, graph);
32
- return tsmember('readonly <%= name %>: <%= variant %> | null;', {
33
- name: propertyName(component.name),
34
- variant
35
- });
36
- }
37
- function renderComponentVariant(component, model, graph) {
38
- const instance = renderComponentInstance(component, model, graph);
39
- const detailFields = extractDetailFields(component.payload);
40
- if (detailFields.length === 0) {
41
- return tstype('{ readonly instance: <%= i %> }', { i: instance });
42
- }
43
- const details = renderDetailFields(detailFields);
44
- return tstype('{ readonly instance: <%= i %>; readonly details: <%= d %> }', {
45
- i: instance,
46
- d: details
47
- });
48
- }
49
- function renderComponentInstance(component, model, graph) {
50
- rejectUnsupportedDependencyPaths(component);
51
- const observedValues = perKeyValueSets(variantsFor(component, graph));
52
- const members = [];
53
- observedValues.forEach((values, key) => {
54
- members.push(tsmember('readonly <%= name %>: <%= type %>;', {
55
- name: propertyName(key),
56
- type: renderOptionValueType(key, values, model)
57
- }));
58
- });
59
- return ts.factory.createTypeLiteralNode(members);
60
- }
61
- function rejectUnsupportedDependencyPaths(component) {
62
- const paths = component.paths ?? [];
63
- paths.forEach((path) => {
64
- if (path.length !== 1) {
65
- throw new Error(`codegen: multi-segment dependency paths are not yet supported (component '${component.name}', path ${JSON.stringify(path)})`);
66
- }
67
- const [head] = path;
68
- check.assert(v.string(), head, `codegen invariant: component dependency path head must be a string (component '${component.name}', path ${JSON.stringify(path)})! ❌`);
69
- });
70
- }
71
- function perKeyValueSets(variants) {
72
- const result = new Map();
73
- variants.forEach((instance) => {
74
- Object.entries(instance).forEach(([key, value]) => {
75
- const existing = result.get(key) ?? new Set();
76
- existing.add(value);
77
- result.set(key, existing);
78
- });
79
- });
80
- return result;
81
- }
82
- function renderOptionValueType(name, observedValues, model) {
83
- const option = model.options.find((candidate) => candidate.name === name);
84
- check.truthy(option, `codegen invariant: option '${name}' referenced by a component must be defined in the model! ❌`);
85
- return renderOptionValueShape(option.payload, observedValues);
86
- }
87
- function renderOptionValueShape(payload, observedValues) {
88
- if (payload.type === 'toggle') {
89
- return maybeNullable(primitiveTypeNode(payload.valueType), payload.isOptional);
90
- }
91
- if (payload.type === 'select') {
92
- const literals = Array.from(observedValues)
93
- .filter((value) => typeof value === 'string')
94
- .map((value) => {
95
- return ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(value));
96
- });
97
- return maybeNullable(unionOf(literals), payload.isOptional);
98
- }
99
- if (payload.type === 'enable' || payload.type === 'unless') {
100
- return renderOptionValueShape(payload.payload, observedValues);
101
- }
102
- const branches = Object.values(payload.selectMap).filter((branch) => branch != null);
103
- const [first] = branches;
104
- if (first == null) {
105
- return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword);
106
- }
107
- return renderOptionValueShape(first, observedValues);
108
- }
109
- function extractDetailFields(payload) {
110
- if (!('type' in payload)) {
111
- return payload.fields;
112
- }
113
- if (payload.type === 'enable' || payload.type === 'unless') {
114
- return extractDetailFields(payload.payload);
115
- }
116
- const branches = Object.values(payload.selectMap).filter((branch) => branch != null);
117
- const [first] = branches;
118
- if (first == null) {
119
- return [];
120
- }
121
- return extractDetailFields(first);
122
- }
123
- function renderDetailFields(fields) {
124
- const members = fields.map((field) => {
125
- return tsmember('readonly <%= name %>: <%= type %>;', {
126
- name: propertyName(field.name),
127
- type: maybeNullable(primitiveTypeNode(field.valueType), field.isOptional)
128
- });
129
- });
130
- return ts.factory.createTypeLiteralNode(members);
131
- }
132
- function renderCollectionMember(collection) {
133
- const config = extractCollectionConfig(collection.payload);
134
- const itemNode = renderModelOrder(config.model);
135
- const tupleSize = fixedTupleSize(config.min, config.max);
136
- const value = tupleSize == null
137
- ? tstype('ReadonlyArray<<%= i %>>', { i: itemNode })
138
- : tstype('readonly [%= items %]', {
139
- items: Array.from({ length: tupleSize }, () => itemNode)
140
- });
141
- return tsmember('readonly <%= name %>: <%= value %>;', {
142
- name: propertyName(collection.name),
143
- value
144
- });
145
- }
146
- function extractCollectionConfig(payload) {
147
- if (!('type' in payload)) {
148
- return payload;
149
- }
150
- if (payload.type === 'enable' || payload.type === 'unless') {
151
- return extractCollectionConfig(payload.payload);
152
- }
153
- const branches = Object.values(payload.selectMap).filter((branch) => branch != null);
154
- const [first] = branches;
155
- if (first == null) {
156
- throw new Error(`codegen: collection 'match' has no non-null branch`);
157
- }
158
- return extractCollectionConfig(first);
159
- }
160
- function fixedTupleSize(min, max) {
161
- if (min !== max) {
162
- return null;
163
- }
164
- if (!Number.isInteger(min) || min < 0) {
165
- return null;
166
- }
167
- return min;
168
- }
169
- function propertyName(name) {
170
- if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) {
171
- return ts.factory.createIdentifier(name);
172
- }
173
- return ts.factory.createStringLiteral(name);
174
- }
175
- function primitiveTypeNode(valueType) {
176
- if (valueType === 'string') {
177
- return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
178
- }
179
- if (valueType === 'number') {
180
- return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
181
- }
182
- return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
183
- }
184
- function unionOf(members) {
185
- const [first, ...rest] = members;
186
- if (first == null) {
187
- return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword);
188
- }
189
- if (rest.length === 0) {
190
- return first;
191
- }
192
- return ts.factory.createUnionTypeNode([first, ...rest]);
193
- }
194
- function maybeNullable(node, isNullable) {
195
- if (!isNullable) {
196
- return node;
197
- }
198
- return ts.factory.createUnionTypeNode([
199
- node,
200
- ts.factory.createLiteralTypeNode(ts.factory.createNull())
201
- ]);
202
- }
203
- function pascalCase(name) {
204
- const words = name.split(/[\s\-_]+/).filter((word) => word.length > 0);
205
- if (words.length === 0) {
206
- throw new Error(`codegen: name '${name}' produces an empty type name`);
207
- }
208
- return words
209
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
210
- .join('')
211
- .replace(/[^A-Za-z0-9]/g, '');
212
- }
@@ -1,5 +0,0 @@
1
- import type ts from 'typescript';
2
- type TemplateData = Readonly<Record<string, string | ts.Node | ReadonlyArray<ts.Node>>>;
3
- export declare function tstype(template: string, data?: TemplateData): ts.TypeNode;
4
- export declare function tsmember(template: string, data?: TemplateData): ts.TypeElement;
5
- export {};
@@ -1,17 +0,0 @@
1
- import { tsquery } from '@phenomnomnominal/tsquery';
2
- import { tstemplate } from '@phenomnomnominal/tstemplate';
3
- import { check } from '../check.js';
4
- export function tstype(template, data = {}) {
5
- const sourceFile = tstemplate(`type __ = ${template};`, data);
6
- const [declaration] = tsquery(sourceFile, 'TypeAliasDeclaration');
7
- check.truthy(declaration, `tstype invariant: template produced no TypeAliasDeclaration! ❌`);
8
- return declaration.type;
9
- }
10
- export function tsmember(template, data = {}) {
11
- const sourceFile = tstemplate(`type __ = { ${template} };`, data);
12
- const [literal] = tsquery(sourceFile, 'TypeLiteral');
13
- check.truthy(literal, `tsmember invariant: template produced no TypeLiteral! ❌`);
14
- const [member] = literal.members;
15
- check.truthy(member, `tsmember invariant: template produced an empty TypeLiteral! ❌`);
16
- return member;
17
- }
@@ -1,7 +0,0 @@
1
- import type { BuilderModel } from '../model/model';
2
- import type { BuilderModelState } from '../model/state';
3
- import type { BuilderUI } from '../ui/ui';
4
- import type { BuilderBindings } from './bind';
5
- import { Builder } from './builder.js';
6
- export declare function builder(): Builder<BuilderModelState>;
7
- export declare function builder<const State extends BuilderModelState>(model: BuilderModel<State>, ui?: BuilderUI, bindings?: BuilderBindings): Builder<State>;
@@ -1,4 +0,0 @@
1
- import { Builder } from './builder.js';
2
- export function builder(model, ui, bindings) {
3
- return new Builder(model, ui, bindings);
4
- }