@foro-sh/foro 0.1.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/_generated-manifest-cases.d.ts +166 -0
- package/dist/_generated-manifest-cases.d.ts.map +1 -0
- package/dist/_generated-manifest-cases.js +192 -0
- package/dist/manifest-cases.d.ts +32 -0
- package/dist/manifest-cases.d.ts.map +1 -0
- package/dist/manifest-cases.js +16 -0
- package/dist/manifest-cases.test.d.ts +2 -0
- package/dist/manifest-cases.test.d.ts.map +1 -0
- package/dist/manifest-cases.test.js +23 -0
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
This package is the TypeScript SDK for Foro.
|
|
4
4
|
|
|
5
|
+
## `@foro-sh/foro/manifest-cases`
|
|
6
|
+
|
|
7
|
+
The shared `foro.yaml` validation table, as typed data. Foro's Python
|
|
8
|
+
`_manifest.py` is a port of [foro-sh/platform]'s
|
|
9
|
+
`apps/api/src/services/manifest.ts`, so both implementations run this same
|
|
10
|
+
table to guarantee they never disagree about what a valid manifest is — if
|
|
11
|
+
they did, `foro check` would pass locally and the deploy would fail.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { manifestCases } from '@foro-sh/foro/manifest-cases'
|
|
15
|
+
|
|
16
|
+
for (const { name, yaml, expect } of manifestCases) {
|
|
17
|
+
// expect is { ok: true } or { ok: false, reason: ManifestRejectionReason }
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Each case asserts only accept/reject and the rejection reason; the resolved
|
|
22
|
+
defaults an implementation produces stay covered by its own tests.
|
|
23
|
+
|
|
24
|
+
The cases live in `packages/python/src/foro/manifest-cases.json` — the single
|
|
25
|
+
source of truth — and are inlined into this package at build time. Add cases
|
|
26
|
+
there, not here.
|
|
27
|
+
|
|
28
|
+
[foro-sh/platform]: https://github.com/foro-sh/platform
|
|
29
|
+
|
|
5
30
|
## Development
|
|
6
31
|
|
|
7
32
|
```bash
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
export declare const rawManifestCases: readonly [{
|
|
2
|
+
readonly name: "minimal-valid";
|
|
3
|
+
readonly yaml: "name: my-server\nentrypoint: server.py\n";
|
|
4
|
+
readonly expect: {
|
|
5
|
+
readonly ok: true;
|
|
6
|
+
};
|
|
7
|
+
}, {
|
|
8
|
+
readonly name: "nested-entrypoint";
|
|
9
|
+
readonly yaml: "name: my-server\nentrypoint: src/server.py\n";
|
|
10
|
+
readonly expect: {
|
|
11
|
+
readonly ok: true;
|
|
12
|
+
};
|
|
13
|
+
}, {
|
|
14
|
+
readonly name: "all-fields";
|
|
15
|
+
readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: app\npython_version: \"3.11\"\nport: 9000\ndependency_manager: poetry\n";
|
|
16
|
+
readonly expect: {
|
|
17
|
+
readonly ok: true;
|
|
18
|
+
};
|
|
19
|
+
}, {
|
|
20
|
+
readonly name: "unquoted-python-version";
|
|
21
|
+
readonly yaml: "name: my-server\nentrypoint: server.py\npython_version: 3.12\n";
|
|
22
|
+
readonly expect: {
|
|
23
|
+
readonly ok: true;
|
|
24
|
+
};
|
|
25
|
+
}, {
|
|
26
|
+
readonly name: "bad-name-uppercase";
|
|
27
|
+
readonly yaml: "name: My-Server\nentrypoint: server.py\n";
|
|
28
|
+
readonly expect: {
|
|
29
|
+
readonly ok: false;
|
|
30
|
+
readonly reason: "invalid_name";
|
|
31
|
+
};
|
|
32
|
+
}, {
|
|
33
|
+
readonly name: "bad-name-too-short";
|
|
34
|
+
readonly yaml: "name: ab\nentrypoint: server.py\n";
|
|
35
|
+
readonly expect: {
|
|
36
|
+
readonly ok: false;
|
|
37
|
+
readonly reason: "invalid_name";
|
|
38
|
+
};
|
|
39
|
+
}, {
|
|
40
|
+
readonly name: "missing-name";
|
|
41
|
+
readonly yaml: "entrypoint: server.py\n";
|
|
42
|
+
readonly expect: {
|
|
43
|
+
readonly ok: false;
|
|
44
|
+
readonly reason: "invalid_name";
|
|
45
|
+
};
|
|
46
|
+
}, {
|
|
47
|
+
readonly name: "bad-entrypoint-traversal";
|
|
48
|
+
readonly yaml: "name: my-server\nentrypoint: ../server.py\n";
|
|
49
|
+
readonly expect: {
|
|
50
|
+
readonly ok: false;
|
|
51
|
+
readonly reason: "invalid_entrypoint";
|
|
52
|
+
};
|
|
53
|
+
}, {
|
|
54
|
+
readonly name: "bad-entrypoint-absolute";
|
|
55
|
+
readonly yaml: "name: my-server\nentrypoint: /server.py\n";
|
|
56
|
+
readonly expect: {
|
|
57
|
+
readonly ok: false;
|
|
58
|
+
readonly reason: "invalid_entrypoint";
|
|
59
|
+
};
|
|
60
|
+
}, {
|
|
61
|
+
readonly name: "bad-entrypoint-shell-metachar";
|
|
62
|
+
readonly yaml: "name: my-server\nentrypoint: \"server.py; rm -rf x\"\n";
|
|
63
|
+
readonly expect: {
|
|
64
|
+
readonly ok: false;
|
|
65
|
+
readonly reason: "invalid_entrypoint";
|
|
66
|
+
};
|
|
67
|
+
}, {
|
|
68
|
+
readonly name: "missing-entrypoint";
|
|
69
|
+
readonly yaml: "name: my-server\n";
|
|
70
|
+
readonly expect: {
|
|
71
|
+
readonly ok: false;
|
|
72
|
+
readonly reason: "invalid_entrypoint";
|
|
73
|
+
};
|
|
74
|
+
}, {
|
|
75
|
+
readonly name: "bad-build-path-traversal";
|
|
76
|
+
readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: ../x\n";
|
|
77
|
+
readonly expect: {
|
|
78
|
+
readonly ok: false;
|
|
79
|
+
readonly reason: "invalid_build_path";
|
|
80
|
+
};
|
|
81
|
+
}, {
|
|
82
|
+
readonly name: "bad-build-path-shell-metachar";
|
|
83
|
+
readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: \". && whoami\"\n";
|
|
84
|
+
readonly expect: {
|
|
85
|
+
readonly ok: false;
|
|
86
|
+
readonly reason: "invalid_build_path";
|
|
87
|
+
};
|
|
88
|
+
}, {
|
|
89
|
+
readonly name: "bad-build-path-space";
|
|
90
|
+
readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: \"my services\"\n";
|
|
91
|
+
readonly expect: {
|
|
92
|
+
readonly ok: false;
|
|
93
|
+
readonly reason: "invalid_build_path";
|
|
94
|
+
};
|
|
95
|
+
}, {
|
|
96
|
+
readonly name: "bad-build-path-newline";
|
|
97
|
+
readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: \"app\\nUSER root\"\n";
|
|
98
|
+
readonly expect: {
|
|
99
|
+
readonly ok: false;
|
|
100
|
+
readonly reason: "invalid_build_path";
|
|
101
|
+
};
|
|
102
|
+
}, {
|
|
103
|
+
readonly name: "bad-python-version";
|
|
104
|
+
readonly yaml: "name: my-server\nentrypoint: server.py\npython_version: \"3.9\"\n";
|
|
105
|
+
readonly expect: {
|
|
106
|
+
readonly ok: false;
|
|
107
|
+
readonly reason: "invalid_python_version";
|
|
108
|
+
};
|
|
109
|
+
}, {
|
|
110
|
+
readonly name: "bad-port-too-low";
|
|
111
|
+
readonly yaml: "name: my-server\nentrypoint: server.py\nport: 80\n";
|
|
112
|
+
readonly expect: {
|
|
113
|
+
readonly ok: false;
|
|
114
|
+
readonly reason: "invalid_port";
|
|
115
|
+
};
|
|
116
|
+
}, {
|
|
117
|
+
readonly name: "bad-port-too-high";
|
|
118
|
+
readonly yaml: "name: my-server\nentrypoint: server.py\nport: 70000\n";
|
|
119
|
+
readonly expect: {
|
|
120
|
+
readonly ok: false;
|
|
121
|
+
readonly reason: "invalid_port";
|
|
122
|
+
};
|
|
123
|
+
}, {
|
|
124
|
+
readonly name: "bad-port-sidecar-reserved";
|
|
125
|
+
readonly yaml: "name: my-server\nentrypoint: server.py\nport: 8001\n";
|
|
126
|
+
readonly expect: {
|
|
127
|
+
readonly ok: false;
|
|
128
|
+
readonly reason: "invalid_port";
|
|
129
|
+
};
|
|
130
|
+
}, {
|
|
131
|
+
readonly name: "bad-port-non-integer";
|
|
132
|
+
readonly yaml: "name: my-server\nentrypoint: server.py\nport: \"abc\"\n";
|
|
133
|
+
readonly expect: {
|
|
134
|
+
readonly ok: false;
|
|
135
|
+
readonly reason: "invalid_port";
|
|
136
|
+
};
|
|
137
|
+
}, {
|
|
138
|
+
readonly name: "bad-dependency-manager";
|
|
139
|
+
readonly yaml: "name: my-server\nentrypoint: server.py\ndependency_manager: pipx\n";
|
|
140
|
+
readonly expect: {
|
|
141
|
+
readonly ok: false;
|
|
142
|
+
readonly reason: "invalid_dependency_manager";
|
|
143
|
+
};
|
|
144
|
+
}, {
|
|
145
|
+
readonly name: "invalid-yaml";
|
|
146
|
+
readonly yaml: "name: [unclosed\n";
|
|
147
|
+
readonly expect: {
|
|
148
|
+
readonly ok: false;
|
|
149
|
+
readonly reason: "invalid_yaml";
|
|
150
|
+
};
|
|
151
|
+
}, {
|
|
152
|
+
readonly name: "invalid-shape-scalar";
|
|
153
|
+
readonly yaml: "just a string\n";
|
|
154
|
+
readonly expect: {
|
|
155
|
+
readonly ok: false;
|
|
156
|
+
readonly reason: "invalid_shape";
|
|
157
|
+
};
|
|
158
|
+
}, {
|
|
159
|
+
readonly name: "invalid-shape-list";
|
|
160
|
+
readonly yaml: "- one\n- two\n";
|
|
161
|
+
readonly expect: {
|
|
162
|
+
readonly ok: false;
|
|
163
|
+
readonly reason: "invalid_shape";
|
|
164
|
+
};
|
|
165
|
+
}];
|
|
166
|
+
//# sourceMappingURL=_generated-manifest-cases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_generated-manifest-cases.d.ts","sourceRoot":"","sources":["../src/_generated-manifest-cases.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6LnB,CAAA"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Generated by scripts/generate-manifest-cases.mjs from the Python package's
|
|
2
|
+
// manifest-cases.json. Do not edit - edit the JSON and rebuild.
|
|
3
|
+
export const rawManifestCases = [
|
|
4
|
+
{
|
|
5
|
+
"name": "minimal-valid",
|
|
6
|
+
"yaml": "name: my-server\nentrypoint: server.py\n",
|
|
7
|
+
"expect": {
|
|
8
|
+
"ok": true
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "nested-entrypoint",
|
|
13
|
+
"yaml": "name: my-server\nentrypoint: src/server.py\n",
|
|
14
|
+
"expect": {
|
|
15
|
+
"ok": true
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "all-fields",
|
|
20
|
+
"yaml": "name: my-server\nentrypoint: server.py\nbuild_path: app\npython_version: \"3.11\"\nport: 9000\ndependency_manager: poetry\n",
|
|
21
|
+
"expect": {
|
|
22
|
+
"ok": true
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "unquoted-python-version",
|
|
27
|
+
"yaml": "name: my-server\nentrypoint: server.py\npython_version: 3.12\n",
|
|
28
|
+
"expect": {
|
|
29
|
+
"ok": true
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "bad-name-uppercase",
|
|
34
|
+
"yaml": "name: My-Server\nentrypoint: server.py\n",
|
|
35
|
+
"expect": {
|
|
36
|
+
"ok": false,
|
|
37
|
+
"reason": "invalid_name"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "bad-name-too-short",
|
|
42
|
+
"yaml": "name: ab\nentrypoint: server.py\n",
|
|
43
|
+
"expect": {
|
|
44
|
+
"ok": false,
|
|
45
|
+
"reason": "invalid_name"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "missing-name",
|
|
50
|
+
"yaml": "entrypoint: server.py\n",
|
|
51
|
+
"expect": {
|
|
52
|
+
"ok": false,
|
|
53
|
+
"reason": "invalid_name"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "bad-entrypoint-traversal",
|
|
58
|
+
"yaml": "name: my-server\nentrypoint: ../server.py\n",
|
|
59
|
+
"expect": {
|
|
60
|
+
"ok": false,
|
|
61
|
+
"reason": "invalid_entrypoint"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "bad-entrypoint-absolute",
|
|
66
|
+
"yaml": "name: my-server\nentrypoint: /server.py\n",
|
|
67
|
+
"expect": {
|
|
68
|
+
"ok": false,
|
|
69
|
+
"reason": "invalid_entrypoint"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "bad-entrypoint-shell-metachar",
|
|
74
|
+
"yaml": "name: my-server\nentrypoint: \"server.py; rm -rf x\"\n",
|
|
75
|
+
"expect": {
|
|
76
|
+
"ok": false,
|
|
77
|
+
"reason": "invalid_entrypoint"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "missing-entrypoint",
|
|
82
|
+
"yaml": "name: my-server\n",
|
|
83
|
+
"expect": {
|
|
84
|
+
"ok": false,
|
|
85
|
+
"reason": "invalid_entrypoint"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "bad-build-path-traversal",
|
|
90
|
+
"yaml": "name: my-server\nentrypoint: server.py\nbuild_path: ../x\n",
|
|
91
|
+
"expect": {
|
|
92
|
+
"ok": false,
|
|
93
|
+
"reason": "invalid_build_path"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "bad-build-path-shell-metachar",
|
|
98
|
+
"yaml": "name: my-server\nentrypoint: server.py\nbuild_path: \". && whoami\"\n",
|
|
99
|
+
"expect": {
|
|
100
|
+
"ok": false,
|
|
101
|
+
"reason": "invalid_build_path"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "bad-build-path-space",
|
|
106
|
+
"yaml": "name: my-server\nentrypoint: server.py\nbuild_path: \"my services\"\n",
|
|
107
|
+
"expect": {
|
|
108
|
+
"ok": false,
|
|
109
|
+
"reason": "invalid_build_path"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "bad-build-path-newline",
|
|
114
|
+
"yaml": "name: my-server\nentrypoint: server.py\nbuild_path: \"app\\nUSER root\"\n",
|
|
115
|
+
"expect": {
|
|
116
|
+
"ok": false,
|
|
117
|
+
"reason": "invalid_build_path"
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "bad-python-version",
|
|
122
|
+
"yaml": "name: my-server\nentrypoint: server.py\npython_version: \"3.9\"\n",
|
|
123
|
+
"expect": {
|
|
124
|
+
"ok": false,
|
|
125
|
+
"reason": "invalid_python_version"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "bad-port-too-low",
|
|
130
|
+
"yaml": "name: my-server\nentrypoint: server.py\nport: 80\n",
|
|
131
|
+
"expect": {
|
|
132
|
+
"ok": false,
|
|
133
|
+
"reason": "invalid_port"
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"name": "bad-port-too-high",
|
|
138
|
+
"yaml": "name: my-server\nentrypoint: server.py\nport: 70000\n",
|
|
139
|
+
"expect": {
|
|
140
|
+
"ok": false,
|
|
141
|
+
"reason": "invalid_port"
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"name": "bad-port-sidecar-reserved",
|
|
146
|
+
"yaml": "name: my-server\nentrypoint: server.py\nport: 8001\n",
|
|
147
|
+
"expect": {
|
|
148
|
+
"ok": false,
|
|
149
|
+
"reason": "invalid_port"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"name": "bad-port-non-integer",
|
|
154
|
+
"yaml": "name: my-server\nentrypoint: server.py\nport: \"abc\"\n",
|
|
155
|
+
"expect": {
|
|
156
|
+
"ok": false,
|
|
157
|
+
"reason": "invalid_port"
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"name": "bad-dependency-manager",
|
|
162
|
+
"yaml": "name: my-server\nentrypoint: server.py\ndependency_manager: pipx\n",
|
|
163
|
+
"expect": {
|
|
164
|
+
"ok": false,
|
|
165
|
+
"reason": "invalid_dependency_manager"
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"name": "invalid-yaml",
|
|
170
|
+
"yaml": "name: [unclosed\n",
|
|
171
|
+
"expect": {
|
|
172
|
+
"ok": false,
|
|
173
|
+
"reason": "invalid_yaml"
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"name": "invalid-shape-scalar",
|
|
178
|
+
"yaml": "just a string\n",
|
|
179
|
+
"expect": {
|
|
180
|
+
"ok": false,
|
|
181
|
+
"reason": "invalid_shape"
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"name": "invalid-shape-list",
|
|
186
|
+
"yaml": "- one\n- two\n",
|
|
187
|
+
"expect": {
|
|
188
|
+
"ok": false,
|
|
189
|
+
"reason": "invalid_shape"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared `foro.yaml` validation table.
|
|
3
|
+
*
|
|
4
|
+
* foro's `_manifest.py` is a port of foro-sh/platform's
|
|
5
|
+
* `apps/api/src/services/manifest.ts`, and the two can never be allowed to
|
|
6
|
+
* silently disagree about what a valid manifest is - if they do, `foro check`
|
|
7
|
+
* passes locally and the deploy fails, which is the exact gap this SDK exists
|
|
8
|
+
* to close. Both sides therefore run the same table: the Python package
|
|
9
|
+
* through `tests/test_manifest_cases.py`, the platform by importing
|
|
10
|
+
* `manifestCases` from here (foro-sh/foro#5).
|
|
11
|
+
*
|
|
12
|
+
* The cases assert only accept/reject and the rejection reason - the resolved
|
|
13
|
+
* defaults each implementation produces stay covered by its own tests.
|
|
14
|
+
*/
|
|
15
|
+
/** Mirrors `ManifestRejectionReason` in foro-sh/platform's `@foro/types`.
|
|
16
|
+
* Kept in sync deliberately: it is what makes a reason added on one side a
|
|
17
|
+
* compile error on the other. */
|
|
18
|
+
export type ManifestRejectionReason = 'missing_manifest' | 'unsupported_language' | 'invalid_yaml' | 'invalid_shape' | 'invalid_name' | 'invalid_entrypoint' | 'invalid_build_path' | 'invalid_python_version' | 'invalid_port' | 'invalid_dependency_manager' | 'unsupported_project';
|
|
19
|
+
export interface ManifestCase {
|
|
20
|
+
/** Stable identifier, unique across the table - use it as the test name. */
|
|
21
|
+
readonly name: string;
|
|
22
|
+
/** Verbatim `foro.yaml` contents to validate. */
|
|
23
|
+
readonly yaml: string;
|
|
24
|
+
readonly expect: {
|
|
25
|
+
readonly ok: true;
|
|
26
|
+
} | {
|
|
27
|
+
readonly ok: false;
|
|
28
|
+
readonly reason: ManifestRejectionReason;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export declare const manifestCases: readonly ManifestCase[];
|
|
32
|
+
//# sourceMappingURL=manifest-cases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-cases.d.ts","sourceRoot":"","sources":["../src/manifest-cases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;kCAEkC;AAClC,MAAM,MAAM,uBAAuB,GAC/B,kBAAkB,GAClB,sBAAsB,GACtB,cAAc,GACd,eAAe,GACf,cAAc,GACd,oBAAoB,GACpB,oBAAoB,GACpB,wBAAwB,GACxB,cAAc,GACd,4BAA4B,GAC5B,qBAAqB,CAAA;AAEzB,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EACX;QAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;KAAE,GACrB;QAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAA;KAAE,CAAA;CACrE;AAED,eAAO,MAAM,aAAa,EAAE,SAAS,YAAY,EAAqB,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared `foro.yaml` validation table.
|
|
3
|
+
*
|
|
4
|
+
* foro's `_manifest.py` is a port of foro-sh/platform's
|
|
5
|
+
* `apps/api/src/services/manifest.ts`, and the two can never be allowed to
|
|
6
|
+
* silently disagree about what a valid manifest is - if they do, `foro check`
|
|
7
|
+
* passes locally and the deploy fails, which is the exact gap this SDK exists
|
|
8
|
+
* to close. Both sides therefore run the same table: the Python package
|
|
9
|
+
* through `tests/test_manifest_cases.py`, the platform by importing
|
|
10
|
+
* `manifestCases` from here (foro-sh/foro#5).
|
|
11
|
+
*
|
|
12
|
+
* The cases assert only accept/reject and the rejection reason - the resolved
|
|
13
|
+
* defaults each implementation produces stay covered by its own tests.
|
|
14
|
+
*/
|
|
15
|
+
import { rawManifestCases } from './_generated-manifest-cases.js';
|
|
16
|
+
export const manifestCases = rawManifestCases;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-cases.test.d.ts","sourceRoot":"","sources":["../src/manifest-cases.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import { manifestCases } from './manifest-cases.js';
|
|
4
|
+
// Sanity checks on the shared table itself - the generator
|
|
5
|
+
// (scripts/generate-manifest-cases.mjs) already enforces these invariants at
|
|
6
|
+
// build time, but that only runs when someone builds this package locally.
|
|
7
|
+
// A real `.test.ts` here means CI catches a malformed table on every push,
|
|
8
|
+
// not just when a maintainer happens to run `npm run build`.
|
|
9
|
+
test('manifestCases is non-empty', () => {
|
|
10
|
+
assert.ok(manifestCases.length > 0);
|
|
11
|
+
});
|
|
12
|
+
test('every case name is unique', () => {
|
|
13
|
+
const names = manifestCases.map((c) => c.name);
|
|
14
|
+
assert.equal(new Set(names).size, names.length);
|
|
15
|
+
});
|
|
16
|
+
test('every case has non-empty yaml and a well-formed expect', () => {
|
|
17
|
+
for (const testCase of manifestCases) {
|
|
18
|
+
assert.ok(testCase.yaml.length > 0, `${testCase.name}: empty yaml`);
|
|
19
|
+
if (testCase.expect.ok)
|
|
20
|
+
continue;
|
|
21
|
+
assert.ok(typeof testCase.expect.reason === 'string' && testCase.expect.reason.length > 0, `${testCase.name}: missing rejection reason`);
|
|
22
|
+
}
|
|
23
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foro-sh/foro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "TypeScript SDK for Foro",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -10,13 +10,18 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./manifest-cases": {
|
|
15
|
+
"types": "./dist/manifest-cases.d.ts",
|
|
16
|
+
"import": "./dist/manifest-cases.js",
|
|
17
|
+
"default": "./dist/manifest-cases.js"
|
|
13
18
|
}
|
|
14
19
|
},
|
|
15
20
|
"files": [
|
|
16
21
|
"dist"
|
|
17
22
|
],
|
|
18
23
|
"scripts": {
|
|
19
|
-
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"build": "node scripts/generate-manifest-cases.mjs && tsc -p tsconfig.json",
|
|
20
25
|
"prepublishOnly": "npm run build",
|
|
21
26
|
"test": "node --test dist/**/*.test.js"
|
|
22
27
|
},
|
|
@@ -42,6 +47,7 @@
|
|
|
42
47
|
"node": ">=18"
|
|
43
48
|
},
|
|
44
49
|
"devDependencies": {
|
|
50
|
+
"@types/node": "^20.11.0",
|
|
45
51
|
"typescript": "^5.6.3"
|
|
46
52
|
}
|
|
47
|
-
}
|
|
53
|
+
}
|