@ekanos/cli 0.16.0 → 0.17.0
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 +62 -4
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +118 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/audiences.d.ts +31 -0
- package/dist/commands/audiences.js +239 -0
- package/dist/commands/audiences.js.map +1 -0
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.js +41 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/check.d.ts +2 -0
- package/dist/commands/check.js +43 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/codegen.d.ts +2 -0
- package/dist/commands/codegen.js +49 -0
- package/dist/commands/codegen.js.map +1 -0
- package/dist/commands/contract.d.ts +52 -0
- package/dist/commands/contract.js +300 -0
- package/dist/commands/contract.js.map +1 -0
- package/dist/commands/dev.d.ts +7 -0
- package/dist/commands/dev.js +186 -12
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/index-experience.d.ts +31 -0
- package/dist/commands/index-experience.js +21 -0
- package/dist/commands/index-experience.js.map +1 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +12 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/new.d.ts +2 -0
- package/dist/commands/new.js +39 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/publish.d.ts +59 -0
- package/dist/commands/publish.js +118 -65
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/scope-notice.d.ts +6 -0
- package/dist/commands/scope-notice.js +15 -0
- package/dist/commands/scope-notice.js.map +1 -1
- package/dist/commands/sources.js +2 -0
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +150 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/teams-audience.d.ts +12 -0
- package/dist/commands/teams-audience.js +144 -0
- package/dist/commands/teams-audience.js.map +1 -0
- package/dist/commands/teams-scope.js +4 -0
- package/dist/commands/teams-scope.js.map +1 -1
- package/dist/commands/teams.d.ts +1 -1
- package/dist/commands/teams.js +6 -1
- package/dist/commands/teams.js.map +1 -1
- package/dist/compatibility.d.ts +10 -1
- package/dist/compatibility.js +10 -1
- package/dist/compatibility.js.map +1 -1
- package/dist/dev-experience.d.ts +125 -0
- package/dist/dev-experience.js +128 -0
- package/dist/dev-experience.js.map +1 -0
- package/dist/experience/boundary.d.ts +12 -0
- package/dist/experience/boundary.js +260 -0
- package/dist/experience/boundary.js.map +1 -0
- package/dist/experience/build.d.ts +16 -0
- package/dist/experience/build.js +90 -0
- package/dist/experience/build.js.map +1 -0
- package/dist/experience/checks.d.ts +29 -0
- package/dist/experience/checks.js +133 -0
- package/dist/experience/checks.js.map +1 -0
- package/dist/experience/contract-sources.d.ts +36 -0
- package/dist/experience/contract-sources.js +111 -0
- package/dist/experience/contract-sources.js.map +1 -0
- package/dist/experience/definition-checks.d.ts +26 -0
- package/dist/experience/definition-checks.js +201 -0
- package/dist/experience/definition-checks.js.map +1 -0
- package/dist/experience/definition-shape.d.ts +60 -0
- package/dist/experience/definition-shape.js +2 -0
- package/dist/experience/definition-shape.js.map +1 -0
- package/dist/experience/init.d.ts +10 -0
- package/dist/experience/init.js +64 -0
- package/dist/experience/init.js.map +1 -0
- package/dist/experience/integrations-codegen.d.ts +17 -0
- package/dist/experience/integrations-codegen.js +51 -0
- package/dist/experience/integrations-codegen.js.map +1 -0
- package/dist/experience/json-schema-to-ts.d.ts +1 -0
- package/dist/experience/json-schema-to-ts.js +90 -0
- package/dist/experience/json-schema-to-ts.js.map +1 -0
- package/dist/experience/load-experience.d.ts +23 -0
- package/dist/experience/load-experience.js +115 -0
- package/dist/experience/load-experience.js.map +1 -0
- package/dist/experience/overrides.d.ts +8 -0
- package/dist/experience/overrides.js +21 -0
- package/dist/experience/overrides.js.map +1 -0
- package/dist/experience/pin-integration.d.ts +8 -0
- package/dist/experience/pin-integration.js +17 -0
- package/dist/experience/pin-integration.js.map +1 -0
- package/dist/experience/pin-ops.d.ts +12 -0
- package/dist/experience/pin-ops.js +36 -0
- package/dist/experience/pin-ops.js.map +1 -0
- package/dist/experience/pins.d.ts +35 -0
- package/dist/experience/pins.js +91 -0
- package/dist/experience/pins.js.map +1 -0
- package/dist/experience/project.d.ts +52 -0
- package/dist/experience/project.js +149 -0
- package/dist/experience/project.js.map +1 -0
- package/dist/experience/publish.d.ts +51 -0
- package/dist/experience/publish.js +144 -0
- package/dist/experience/publish.js.map +1 -0
- package/dist/experience/regenerate.d.ts +10 -0
- package/dist/experience/regenerate.js +33 -0
- package/dist/experience/regenerate.js.map +1 -0
- package/dist/experience/report.d.ts +9 -0
- package/dist/experience/report.js +23 -0
- package/dist/experience/report.js.map +1 -0
- package/dist/harness-scaffold.d.ts +52 -18
- package/dist/harness-scaffold.js +189 -9
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/index.js +139 -7
- package/dist/index.js.map +1 -1
- package/dist/project-checks.d.ts +12 -0
- package/dist/project-checks.js +46 -0
- package/dist/project-checks.js.map +1 -1
- package/dist/project.d.ts +14 -0
- package/dist/project.js +10 -0
- package/dist/project.js.map +1 -1
- package/dist/repo/publish.d.ts +42 -0
- package/dist/repo/publish.js +225 -0
- package/dist/repo/publish.js.map +1 -0
- package/dist/repo/scaffold.d.ts +30 -0
- package/dist/repo/scaffold.js +165 -0
- package/dist/repo/scaffold.js.map +1 -0
- package/dist/teams-api.d.ts +37 -1
- package/dist/teams-api.js +62 -1
- package/dist/teams-api.js.map +1 -1
- package/dist/validate-findings.js +2 -1
- package/dist/validate-findings.js.map +1 -1
- package/package.json +2 -2
- package/templates/ekanos.json.tmpl +1 -0
- package/templates/experience/AGENTS.md.tmpl +22 -0
- package/templates/experience/ekanos.json.tmpl +7 -0
- package/templates/experience/ekanos.lock.json.tmpl +4 -0
- package/templates/experience/eslint.config.mjs.tmpl +25 -0
- package/templates/experience/gitignore.tmpl +17 -0
- package/templates/experience/package.json.tmpl +41 -0
- package/templates/experience/src/.ekanos/integrations.d.ts.tmpl +3 -0
- package/templates/experience/src/experience.ts.tmpl +30 -0
- package/templates/experience/src/widgets/hello-widget.tsx.tmpl +40 -0
- package/templates/experience/tsconfig.json.tmpl +20 -0
- package/templates/harness/app/%5F%5Fekanos/api/[...path]/route.ts.tmpl +9 -0
- package/templates/harness/app/[slug]/activation/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/tile/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/triggers/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/widgets/[widgetId]/page.tsx.tmpl +1 -1
- package/templates/harness/app/[slug]/widgets/page.tsx.tmpl +1 -1
- package/templates/harness/app/experience/layout.tsx.tmpl +17 -0
- package/templates/harness/app/experience/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/pages/[id]/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/review-queue/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/runs/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/widgets/[id]/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/widgets/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/workflows/[id]/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/workflows/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/connect/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/events/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/health/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/operations/[kind]/[name]/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/operations/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/activation/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/layout.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/tile/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/triggers/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/widgets/[widgetId]/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/preview/widgets/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/[slug]/runs/page.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/layout.tsx.tmpl +1 -0
- package/templates/harness/app/integrations/page.tsx.tmpl +1 -0
- package/templates/harness/app/page.experience.tsx.tmpl +5 -0
- package/templates/harness/repo/app/%5F%5Fekanos/api/[...path]/route.ts.tmpl +11 -0
- package/templates/harness/repo/app/(repo)/connections/page.tsx.tmpl +1 -0
- package/templates/harness/repo/app/(repo)/layout.tsx.tmpl +1 -0
- package/templates/harness/repo/app/(repo)/page.tsx.tmpl +1 -0
- package/templates/harness/repo/app/experiences/[name]/connect/page.tsx.tmpl +1 -0
- package/templates/harness/repo/app/experiences/[name]/layout.tsx.tmpl +9 -0
- package/templates/harness/repo/app/experiences/page.tsx.tmpl +5 -0
- package/templates/harness/repo/app/harness-shell.tsx.tmpl +9 -0
- package/templates/harness/repo/app/integrations/[slug]/layout.tsx.tmpl +1 -0
- package/templates/harness/repo/app/integrations/page.tsx.tmpl +5 -0
- package/templates/harness/repo/app/repo-experiences.ts.tmpl +5 -0
- package/templates/harness/styles/globals.css.tmpl +15 -1
- package/templates/package.json.tmpl +4 -4
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { build } from 'esbuild';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { isContained } from '../safe-fs.js';
|
|
5
|
+
export const BOUNDARY_CHECK = 'checks/experience-boundary';
|
|
6
|
+
/** Packages that may stay external at runtime (the host provides them). */
|
|
7
|
+
export const ALLOWED_EXTERNALS = [
|
|
8
|
+
'react',
|
|
9
|
+
'react-dom',
|
|
10
|
+
'@ekanos/sdk',
|
|
11
|
+
'@ekanos/ui',
|
|
12
|
+
'zod',
|
|
13
|
+
'@tanstack/react-query',
|
|
14
|
+
'next-intl',
|
|
15
|
+
'react-hook-form',
|
|
16
|
+
'@hookform/resolvers',
|
|
17
|
+
];
|
|
18
|
+
const INTEGRATION_PACKAGE = /^@companydotcom\/integration-[a-z0-9-]+/;
|
|
19
|
+
/** A pnpm-store or plain node_modules path of an integration package. */
|
|
20
|
+
const INTEGRATION_IN_PATH = /@companydotcom[/+]integration-[a-z0-9-]+/;
|
|
21
|
+
const HOST_INTERNAL = /^@kit\//;
|
|
22
|
+
const SOURCE_EXT = /\.(?:[cm]?[jt]sx?)$/;
|
|
23
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', '.ekanos', '.git']);
|
|
24
|
+
const finding = (message, hint, file, line) => (Object.assign(Object.assign({ check: BOUNDARY_CHECK, severity: 'error', message,
|
|
25
|
+
hint }, (file ? { file } : {})), (line ? { line } : {})));
|
|
26
|
+
const INTEGRATION_HINT = 'An experience reaches an integration only through its pinned contract: useOperation / operation() / step.tool. Add the integration with "ekanos add <integration>@<version>".';
|
|
27
|
+
/** Nearest `ekanos.json` at or above `start`, with its kind, or null. */
|
|
28
|
+
export function nearestEkanosKind(start) {
|
|
29
|
+
var _a;
|
|
30
|
+
let current = fs.existsSync(start) && fs.statSync(start).isDirectory()
|
|
31
|
+
? start
|
|
32
|
+
: path.dirname(start);
|
|
33
|
+
for (;;) {
|
|
34
|
+
const file = path.join(current, 'ekanos.json');
|
|
35
|
+
if (fs.existsSync(file)) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
38
|
+
return { dir: current, kind: (_a = parsed.kind) !== null && _a !== void 0 ? _a : 'integration' };
|
|
39
|
+
}
|
|
40
|
+
catch (_b) {
|
|
41
|
+
return { dir: current, kind: 'integration' };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const parent = path.dirname(current);
|
|
45
|
+
if (parent === current)
|
|
46
|
+
return null;
|
|
47
|
+
current = parent;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// ─── 1. package.json ────────────────────────────────────────────────────────
|
|
51
|
+
const DEP_FIELDS = [
|
|
52
|
+
'dependencies',
|
|
53
|
+
'devDependencies',
|
|
54
|
+
'peerDependencies',
|
|
55
|
+
'optionalDependencies',
|
|
56
|
+
];
|
|
57
|
+
function checkPackageJson(project) {
|
|
58
|
+
const file = path.join(project.dir, 'package.json');
|
|
59
|
+
if (!fs.existsSync(file))
|
|
60
|
+
return [];
|
|
61
|
+
const findings = [];
|
|
62
|
+
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
63
|
+
const specs = [];
|
|
64
|
+
for (const field of DEP_FIELDS) {
|
|
65
|
+
const block = pkg[field];
|
|
66
|
+
if (block && typeof block === 'object') {
|
|
67
|
+
specs.push(...Object.entries(block));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const pnpm = pkg.pnpm;
|
|
71
|
+
for (const block of [
|
|
72
|
+
pnpm === null || pnpm === void 0 ? void 0 : pnpm.overrides,
|
|
73
|
+
pkg.overrides,
|
|
74
|
+
]) {
|
|
75
|
+
if (block && typeof block === 'object')
|
|
76
|
+
specs.push(...Object.entries(block));
|
|
77
|
+
}
|
|
78
|
+
for (const [name, spec] of specs) {
|
|
79
|
+
if (INTEGRATION_PACKAGE.test(name)) {
|
|
80
|
+
findings.push(finding(`package.json depends on the integration package ${name}.`, INTEGRATION_HINT, 'package.json'));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const linked = /^(?:file|link|workspace):(.*)$/.exec(String(spec));
|
|
84
|
+
if ((linked === null || linked === void 0 ? void 0 : linked[1]) !== undefined && linked[1] !== '*' && linked[1] !== '^') {
|
|
85
|
+
const target = path.resolve(project.dir, linked[1]);
|
|
86
|
+
const kind = nearestEkanosKind(target);
|
|
87
|
+
if (kind &&
|
|
88
|
+
kind.kind === 'integration' &&
|
|
89
|
+
path.resolve(kind.dir) !== project.dir) {
|
|
90
|
+
findings.push(finding(`package.json links ${name} to ${linked[1]}, an integration project.`, INTEGRATION_HINT, 'package.json'));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// A resolved dependency that is itself an integration, whatever its name.
|
|
94
|
+
const installed = path.join(project.dir, 'node_modules', name, 'ekanos.json');
|
|
95
|
+
if (fs.existsSync(installed)) {
|
|
96
|
+
try {
|
|
97
|
+
const kind = JSON.parse(fs.readFileSync(installed, 'utf8')).kind;
|
|
98
|
+
if ((kind !== null && kind !== void 0 ? kind : 'integration') === 'integration') {
|
|
99
|
+
findings.push(finding(`package.json depends on ${name}, which is an integration (its ekanos.json is kind "integration").`, INTEGRATION_HINT, 'package.json'));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (_a) {
|
|
103
|
+
// unreadable ekanos.json in a dependency: the bundle scan still applies
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return findings;
|
|
108
|
+
}
|
|
109
|
+
// ─── 2. source scan ─────────────────────────────────────────────────────────
|
|
110
|
+
const IMPORT_PATTERN = /(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s+|\brequire\s*\(\s*)(['"`])([^'"`\n]+)\1/g;
|
|
111
|
+
const COMPUTED_IMPORT = /\bimport\s*\(\s*(?!['"`])/g;
|
|
112
|
+
function* walkSources(dir, ignore, root) {
|
|
113
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
114
|
+
if (entry.isSymbolicLink())
|
|
115
|
+
continue;
|
|
116
|
+
const full = path.join(dir, entry.name);
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
if (SKIP_DIRS.has(entry.name))
|
|
119
|
+
continue;
|
|
120
|
+
const rel = path.relative(root, full).split(path.sep).join('/');
|
|
121
|
+
if (ignore.includes(rel))
|
|
122
|
+
continue;
|
|
123
|
+
yield* walkSources(full, ignore, root);
|
|
124
|
+
}
|
|
125
|
+
else if (SOURCE_EXT.test(entry.name)) {
|
|
126
|
+
yield full;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const lineOf = (text, index) => text.slice(0, index).split('\n').length;
|
|
131
|
+
function checkSources(project) {
|
|
132
|
+
var _a, _b, _c;
|
|
133
|
+
const findings = [];
|
|
134
|
+
const srcDir = path.join(project.dir, 'src');
|
|
135
|
+
if (!fs.existsSync(srcDir))
|
|
136
|
+
return findings;
|
|
137
|
+
const ignore = (_a = project.ekanos.ignore) !== null && _a !== void 0 ? _a : [];
|
|
138
|
+
for (const file of walkSources(srcDir, ignore, project.dir)) {
|
|
139
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
140
|
+
const rel = path.relative(project.dir, file).split(path.sep).join('/');
|
|
141
|
+
for (const match of text.matchAll(IMPORT_PATTERN)) {
|
|
142
|
+
const spec = match[2];
|
|
143
|
+
const line = lineOf(text, (_b = match.index) !== null && _b !== void 0 ? _b : 0);
|
|
144
|
+
if (INTEGRATION_PACKAGE.test(spec) || INTEGRATION_IN_PATH.test(spec)) {
|
|
145
|
+
findings.push(finding(`${rel}:${line} imports the integration "${spec}".`, INTEGRATION_HINT, rel, line));
|
|
146
|
+
}
|
|
147
|
+
else if (HOST_INTERNAL.test(spec)) {
|
|
148
|
+
findings.push(finding(`${rel}:${line} imports "${spec}", which is host-internal.`, 'Build with @ekanos/ui and @ekanos/sdk; @kit/* does not exist for a partner.', rel, line));
|
|
149
|
+
}
|
|
150
|
+
else if (spec.startsWith('.')) {
|
|
151
|
+
const target = path.resolve(path.dirname(file), spec);
|
|
152
|
+
if (!isContained(project.dir, target)) {
|
|
153
|
+
findings.push(finding(`${rel}:${line} imports "${spec}", a path outside the project.`, `${INTEGRATION_HINT} Code outside this project is not part of the experience.`, rel, line));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const owner = nearestEkanosKind(target);
|
|
157
|
+
if (owner && owner.kind === 'integration') {
|
|
158
|
+
findings.push(finding(`${rel}:${line} imports "${spec}", inside an integration project.`, INTEGRATION_HINT, rel, line));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const match of text.matchAll(COMPUTED_IMPORT)) {
|
|
164
|
+
const line = lineOf(text, (_c = match.index) !== null && _c !== void 0 ? _c : 0);
|
|
165
|
+
findings.push(finding(`${rel}:${line} uses a computed dynamic import(), which cannot be checked.`, 'Import a static specifier.', rel, line));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return findings;
|
|
169
|
+
}
|
|
170
|
+
// ─── 3. bundle scan ─────────────────────────────────────────────────────────
|
|
171
|
+
function externalsFor(project) {
|
|
172
|
+
var _a;
|
|
173
|
+
const names = [...ALLOWED_EXTERNALS, ...((_a = project.ekanos.allowImports) !== null && _a !== void 0 ? _a : [])];
|
|
174
|
+
return names.flatMap((name) => [name, `${name}/*`]);
|
|
175
|
+
}
|
|
176
|
+
/** Bundle the entry and fail on any input that is integration code. */
|
|
177
|
+
async function checkBundle(project) {
|
|
178
|
+
var _a;
|
|
179
|
+
const entry = path.join(project.dir, project.entry);
|
|
180
|
+
if (!fs.existsSync(entry)) {
|
|
181
|
+
return [
|
|
182
|
+
finding(`entry ${project.entry} does not exist.`, 'Fix "entry" in ekanos.json.', 'ekanos.json'),
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
let result;
|
|
186
|
+
try {
|
|
187
|
+
result = await build({
|
|
188
|
+
entryPoints: [entry],
|
|
189
|
+
bundle: true,
|
|
190
|
+
write: false,
|
|
191
|
+
metafile: true,
|
|
192
|
+
platform: 'browser',
|
|
193
|
+
format: 'esm',
|
|
194
|
+
jsx: 'automatic',
|
|
195
|
+
logLevel: 'silent',
|
|
196
|
+
absWorkingDir: project.dir,
|
|
197
|
+
external: externalsFor(project),
|
|
198
|
+
loader: { '.css': 'empty', '.svg': 'dataurl', '.png': 'dataurl' },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
const errors = (_a = error.errors) !== null && _a !== void 0 ? _a : [];
|
|
203
|
+
if (errors.length === 0) {
|
|
204
|
+
return [
|
|
205
|
+
finding(`the entry could not be bundled: ${error instanceof Error ? error.message : String(error)}`, 'Fix the error and re-run.'),
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
return errors.map((e) => { var _a, _b; return finding(`bundle: ${e.text}`, 'Install the dependency, fix the import, or add a reviewed specifier to ekanos.json "allowImports".', (_a = e.location) === null || _a === void 0 ? void 0 : _a.file, (_b = e.location) === null || _b === void 0 ? void 0 : _b.line); });
|
|
209
|
+
}
|
|
210
|
+
const findings = [];
|
|
211
|
+
const reported = new Set();
|
|
212
|
+
for (const input of Object.keys(result.metafile.inputs)) {
|
|
213
|
+
const absolute = path.resolve(project.dir, input);
|
|
214
|
+
let problem = null;
|
|
215
|
+
if (INTEGRATION_IN_PATH.test(absolute)) {
|
|
216
|
+
problem = 'is integration package code';
|
|
217
|
+
}
|
|
218
|
+
else if (!isContained(project.dir, absolute) &&
|
|
219
|
+
!absolute.includes(`${path.sep}node_modules${path.sep}`)) {
|
|
220
|
+
problem = 'is outside the project';
|
|
221
|
+
}
|
|
222
|
+
else if (absolute.includes(`${path.sep}node_modules${path.sep}`)) {
|
|
223
|
+
const owner = packageRootOf(absolute);
|
|
224
|
+
const kind = owner ? nearestEkanosKind(owner) : null;
|
|
225
|
+
if (kind && kind.dir === owner && kind.kind === 'integration') {
|
|
226
|
+
problem = 'is code of an integration package';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
const owner = nearestEkanosKind(absolute);
|
|
231
|
+
if (owner && owner.kind === 'integration')
|
|
232
|
+
problem = 'is inside an integration project';
|
|
233
|
+
}
|
|
234
|
+
if (problem && !reported.has(input)) {
|
|
235
|
+
reported.add(input);
|
|
236
|
+
findings.push(finding(`the bundle contains ${input}, which ${problem}.`, `${INTEGRATION_HINT} It reached the bundle through an import chain; the source scan cannot always see this.`, input));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return findings;
|
|
240
|
+
}
|
|
241
|
+
function packageRootOf(file) {
|
|
242
|
+
let current = path.dirname(file);
|
|
243
|
+
for (;;) {
|
|
244
|
+
if (fs.existsSync(path.join(current, 'package.json')))
|
|
245
|
+
return current;
|
|
246
|
+
const parent = path.dirname(current);
|
|
247
|
+
if (parent === current)
|
|
248
|
+
return null;
|
|
249
|
+
current = parent;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** The authoritative boundary: package.json, source scan, bundle scan. */
|
|
253
|
+
export async function checkExperienceBoundary(project) {
|
|
254
|
+
return [
|
|
255
|
+
...checkPackageJson(project),
|
|
256
|
+
...checkSources(project),
|
|
257
|
+
...(await checkBundle(project)),
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=boundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundary.js","sourceRoot":"","sources":["../../src/experience/boundary.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\nimport { build } from 'esbuild';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { isContained } from '../safe-fs';\nimport type { ExperienceProject } from './project';\n\nexport const BOUNDARY_CHECK = 'checks/experience-boundary';\n\n/** Packages that may stay external at runtime (the host provides them). */\nexport const ALLOWED_EXTERNALS = [\n 'react',\n 'react-dom',\n '@ekanos/sdk',\n '@ekanos/ui',\n 'zod',\n '@tanstack/react-query',\n 'next-intl',\n 'react-hook-form',\n '@hookform/resolvers',\n] as const;\n\nconst INTEGRATION_PACKAGE = /^@companydotcom\\/integration-[a-z0-9-]+/;\n/** A pnpm-store or plain node_modules path of an integration package. */\nconst INTEGRATION_IN_PATH = /@companydotcom[/+]integration-[a-z0-9-]+/;\nconst HOST_INTERNAL = /^@kit\\//;\n\nconst SOURCE_EXT = /\\.(?:[cm]?[jt]sx?)$/;\nconst SKIP_DIRS = new Set(['node_modules', 'dist', '.ekanos', '.git']);\n\nconst finding = (\n message: string,\n hint: string,\n file?: string,\n line?: number,\n): Finding => ({\n check: BOUNDARY_CHECK,\n severity: 'error',\n message,\n hint,\n ...(file ? { file } : {}),\n ...(line ? { line } : {}),\n});\n\nconst INTEGRATION_HINT =\n 'An experience reaches an integration only through its pinned contract: useOperation / operation() / step.tool. Add the integration with \"ekanos add <integration>@<version>\".';\n\n/** Nearest `ekanos.json` at or above `start`, with its kind, or null. */\nexport function nearestEkanosKind(\n start: string,\n): { dir: string; kind: string } | null {\n let current =\n fs.existsSync(start) && fs.statSync(start).isDirectory()\n ? start\n : path.dirname(start);\n for (;;) {\n const file = path.join(current, 'ekanos.json');\n if (fs.existsSync(file)) {\n try {\n const parsed = JSON.parse(fs.readFileSync(file, 'utf8')) as {\n kind?: string;\n };\n\n return { dir: current, kind: parsed.kind ?? 'integration' };\n } catch {\n return { dir: current, kind: 'integration' };\n }\n }\n const parent = path.dirname(current);\n if (parent === current) return null;\n current = parent;\n }\n}\n\n// ─── 1. package.json ────────────────────────────────────────────────────────\n\nconst DEP_FIELDS = [\n 'dependencies',\n 'devDependencies',\n 'peerDependencies',\n 'optionalDependencies',\n] as const;\n\nfunction checkPackageJson(project: ExperienceProject): Finding[] {\n const file = path.join(project.dir, 'package.json');\n if (!fs.existsSync(file)) return [];\n const findings: Finding[] = [];\n const pkg = JSON.parse(fs.readFileSync(file, 'utf8')) as Record<\n string,\n unknown\n >;\n\n const specs: Array<[string, string]> = [];\n for (const field of DEP_FIELDS) {\n const block = pkg[field];\n if (block && typeof block === 'object') {\n specs.push(...Object.entries(block as Record<string, string>));\n }\n }\n const pnpm = pkg.pnpm as { overrides?: Record<string, string> } | undefined;\n for (const block of [\n pnpm?.overrides,\n pkg.overrides as Record<string, string> | undefined,\n ]) {\n if (block && typeof block === 'object')\n specs.push(...Object.entries(block));\n }\n\n for (const [name, spec] of specs) {\n if (INTEGRATION_PACKAGE.test(name)) {\n findings.push(\n finding(\n `package.json depends on the integration package ${name}.`,\n INTEGRATION_HINT,\n 'package.json',\n ),\n );\n continue;\n }\n const linked = /^(?:file|link|workspace):(.*)$/.exec(String(spec));\n if (linked?.[1] !== undefined && linked[1] !== '*' && linked[1] !== '^') {\n const target = path.resolve(project.dir, linked[1]);\n const kind = nearestEkanosKind(target);\n if (\n kind &&\n kind.kind === 'integration' &&\n path.resolve(kind.dir) !== project.dir\n ) {\n findings.push(\n finding(\n `package.json links ${name} to ${linked[1]}, an integration project.`,\n INTEGRATION_HINT,\n 'package.json',\n ),\n );\n }\n }\n // A resolved dependency that is itself an integration, whatever its name.\n const installed = path.join(\n project.dir,\n 'node_modules',\n name,\n 'ekanos.json',\n );\n if (fs.existsSync(installed)) {\n try {\n const kind = (\n JSON.parse(fs.readFileSync(installed, 'utf8')) as { kind?: string }\n ).kind;\n if ((kind ?? 'integration') === 'integration') {\n findings.push(\n finding(\n `package.json depends on ${name}, which is an integration (its ekanos.json is kind \"integration\").`,\n INTEGRATION_HINT,\n 'package.json',\n ),\n );\n }\n } catch {\n // unreadable ekanos.json in a dependency: the bundle scan still applies\n }\n }\n }\n\n return findings;\n}\n\n// ─── 2. source scan ─────────────────────────────────────────────────────────\n\nconst IMPORT_PATTERN =\n /(?:\\bfrom\\s*|\\bimport\\s*\\(\\s*|\\bimport\\s+|\\brequire\\s*\\(\\s*)(['\"`])([^'\"`\\n]+)\\1/g;\nconst COMPUTED_IMPORT = /\\bimport\\s*\\(\\s*(?!['\"`])/g;\n\nfunction* walkSources(\n dir: string,\n ignore: readonly string[],\n root: string,\n): Generator<string> {\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n if (entry.isSymbolicLink()) continue;\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n const rel = path.relative(root, full).split(path.sep).join('/');\n if (ignore.includes(rel)) continue;\n yield* walkSources(full, ignore, root);\n } else if (SOURCE_EXT.test(entry.name)) {\n yield full;\n }\n }\n}\n\nconst lineOf = (text: string, index: number) =>\n text.slice(0, index).split('\\n').length;\n\nfunction checkSources(project: ExperienceProject): Finding[] {\n const findings: Finding[] = [];\n const srcDir = path.join(project.dir, 'src');\n if (!fs.existsSync(srcDir)) return findings;\n const ignore = project.ekanos.ignore ?? [];\n\n for (const file of walkSources(srcDir, ignore, project.dir)) {\n const text = fs.readFileSync(file, 'utf8');\n const rel = path.relative(project.dir, file).split(path.sep).join('/');\n\n for (const match of text.matchAll(IMPORT_PATTERN)) {\n const spec = match[2] as string;\n const line = lineOf(text, match.index ?? 0);\n\n if (INTEGRATION_PACKAGE.test(spec) || INTEGRATION_IN_PATH.test(spec)) {\n findings.push(\n finding(\n `${rel}:${line} imports the integration \"${spec}\".`,\n INTEGRATION_HINT,\n rel,\n line,\n ),\n );\n } else if (HOST_INTERNAL.test(spec)) {\n findings.push(\n finding(\n `${rel}:${line} imports \"${spec}\", which is host-internal.`,\n 'Build with @ekanos/ui and @ekanos/sdk; @kit/* does not exist for a partner.',\n rel,\n line,\n ),\n );\n } else if (spec.startsWith('.')) {\n const target = path.resolve(path.dirname(file), spec);\n if (!isContained(project.dir, target)) {\n findings.push(\n finding(\n `${rel}:${line} imports \"${spec}\", a path outside the project.`,\n `${INTEGRATION_HINT} Code outside this project is not part of the experience.`,\n rel,\n line,\n ),\n );\n } else {\n const owner = nearestEkanosKind(target);\n if (owner && owner.kind === 'integration') {\n findings.push(\n finding(\n `${rel}:${line} imports \"${spec}\", inside an integration project.`,\n INTEGRATION_HINT,\n rel,\n line,\n ),\n );\n }\n }\n }\n }\n\n for (const match of text.matchAll(COMPUTED_IMPORT)) {\n const line = lineOf(text, match.index ?? 0);\n findings.push(\n finding(\n `${rel}:${line} uses a computed dynamic import(), which cannot be checked.`,\n 'Import a static specifier.',\n rel,\n line,\n ),\n );\n }\n }\n\n return findings;\n}\n\n// ─── 3. bundle scan ─────────────────────────────────────────────────────────\n\nfunction externalsFor(project: ExperienceProject): string[] {\n const names = [...ALLOWED_EXTERNALS, ...(project.ekanos.allowImports ?? [])];\n\n return names.flatMap((name) => [name, `${name}/*`]);\n}\n\n/** Bundle the entry and fail on any input that is integration code. */\nasync function checkBundle(project: ExperienceProject): Promise<Finding[]> {\n const entry = path.join(project.dir, project.entry);\n if (!fs.existsSync(entry)) {\n return [\n finding(\n `entry ${project.entry} does not exist.`,\n 'Fix \"entry\" in ekanos.json.',\n 'ekanos.json',\n ),\n ];\n }\n\n let result;\n try {\n result = await build({\n entryPoints: [entry],\n bundle: true,\n write: false,\n metafile: true,\n platform: 'browser',\n format: 'esm',\n jsx: 'automatic',\n logLevel: 'silent',\n absWorkingDir: project.dir,\n external: externalsFor(project),\n loader: { '.css': 'empty', '.svg': 'dataurl', '.png': 'dataurl' },\n });\n } catch (error) {\n const errors =\n (\n error as {\n errors?: Array<{\n text: string;\n location?: { file: string; line: number } | null;\n }>;\n }\n ).errors ?? [];\n if (errors.length === 0) {\n return [\n finding(\n `the entry could not be bundled: ${error instanceof Error ? error.message : String(error)}`,\n 'Fix the error and re-run.',\n ),\n ];\n }\n\n return errors.map((e) =>\n finding(\n `bundle: ${e.text}`,\n 'Install the dependency, fix the import, or add a reviewed specifier to ekanos.json \"allowImports\".',\n e.location?.file,\n e.location?.line,\n ),\n );\n }\n\n const findings: Finding[] = [];\n const reported = new Set<string>();\n for (const input of Object.keys(result.metafile.inputs)) {\n const absolute = path.resolve(project.dir, input);\n let problem: string | null = null;\n\n if (INTEGRATION_IN_PATH.test(absolute)) {\n problem = 'is integration package code';\n } else if (\n !isContained(project.dir, absolute) &&\n !absolute.includes(`${path.sep}node_modules${path.sep}`)\n ) {\n problem = 'is outside the project';\n } else if (absolute.includes(`${path.sep}node_modules${path.sep}`)) {\n const owner = packageRootOf(absolute);\n const kind = owner ? nearestEkanosKind(owner) : null;\n if (kind && kind.dir === owner && kind.kind === 'integration') {\n problem = 'is code of an integration package';\n }\n } else {\n const owner = nearestEkanosKind(absolute);\n if (owner && owner.kind === 'integration')\n problem = 'is inside an integration project';\n }\n\n if (problem && !reported.has(input)) {\n reported.add(input);\n findings.push(\n finding(\n `the bundle contains ${input}, which ${problem}.`,\n `${INTEGRATION_HINT} It reached the bundle through an import chain; the source scan cannot always see this.`,\n input,\n ),\n );\n }\n }\n\n return findings;\n}\n\nfunction packageRootOf(file: string): string | null {\n let current = path.dirname(file);\n for (;;) {\n if (fs.existsSync(path.join(current, 'package.json'))) return current;\n const parent = path.dirname(current);\n if (parent === current) return null;\n current = parent;\n }\n}\n\n/** The authoritative boundary: package.json, source scan, bundle scan. */\nexport async function checkExperienceBoundary(\n project: ExperienceProject,\n): Promise<Finding[]> {\n return [\n ...checkPackageJson(project),\n ...checkSources(project),\n ...(await checkBundle(project)),\n ];\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ExperienceManifest } from '@ekanos/integration-schema';
|
|
2
|
+
import type { ExperienceCheckResult } from './checks.js';
|
|
3
|
+
import { type ExperienceProject } from './project.js';
|
|
4
|
+
export declare const DIST_ENTRY = "dist/experience.js";
|
|
5
|
+
export declare const DIST_TYPES = "dist/experience.d.ts";
|
|
6
|
+
export declare const MANIFEST_FILE = "manifest.json";
|
|
7
|
+
export interface BuildOutput {
|
|
8
|
+
manifest: ExperienceManifest;
|
|
9
|
+
files: string[];
|
|
10
|
+
changed: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Emit `dist/experience.js` (browser ESM, runtime packages left external),
|
|
14
|
+
* `dist/experience.d.ts` and `manifest.json`. Assumes the checks passed.
|
|
15
|
+
*/
|
|
16
|
+
export declare function emitExperience(project: ExperienceProject, result: ExperienceCheckResult): Promise<BuildOutput>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ExperienceManifestSchema, } from '@ekanos/integration-schema';
|
|
2
|
+
import { build as esbuild } from 'esbuild';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { preconditionError } from '../errors.js';
|
|
6
|
+
import { ALLOWED_EXTERNALS } from './boundary.js';
|
|
7
|
+
import { buildManifest } from './definition-checks.js';
|
|
8
|
+
import { formatJson, writeIfChanged } from './project.js';
|
|
9
|
+
export const DIST_ENTRY = 'dist/experience.js';
|
|
10
|
+
export const DIST_TYPES = 'dist/experience.d.ts';
|
|
11
|
+
export const MANIFEST_FILE = 'manifest.json';
|
|
12
|
+
function readPackageJson(project) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(path.join(project.dir, 'package.json'), 'utf8'));
|
|
15
|
+
}
|
|
16
|
+
catch (_a) {
|
|
17
|
+
throw preconditionError('package.json is missing or unreadable.', 'An experience is an npm package; run "ekanos init --kind experience" or add a package.json.');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function sdkVersion(project, declared) {
|
|
21
|
+
var _a;
|
|
22
|
+
try {
|
|
23
|
+
const installed = JSON.parse(fs.readFileSync(path.join(project.dir, 'node_modules', '@ekanos', 'sdk', 'package.json'), 'utf8'));
|
|
24
|
+
if (installed.version)
|
|
25
|
+
return installed.version;
|
|
26
|
+
}
|
|
27
|
+
catch (_b) {
|
|
28
|
+
// not installed: fall back to the declared range
|
|
29
|
+
}
|
|
30
|
+
return ((_a = declared === null || declared === void 0 ? void 0 : declared['@ekanos/sdk']) !== null && _a !== void 0 ? _a : 'unknown').replace(/^[\^~]/, '');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Emit `dist/experience.js` (browser ESM, runtime packages left external),
|
|
34
|
+
* `dist/experience.d.ts` and `manifest.json`. Assumes the checks passed.
|
|
35
|
+
*/
|
|
36
|
+
export async function emitExperience(project, result) {
|
|
37
|
+
var _a;
|
|
38
|
+
var _b, _c, _d;
|
|
39
|
+
const loaded = result.loaded;
|
|
40
|
+
if (!loaded) {
|
|
41
|
+
throw preconditionError('The experience definition was not loaded.', 'Run "ekanos check" to see why.');
|
|
42
|
+
}
|
|
43
|
+
const pkg = readPackageJson(project);
|
|
44
|
+
const version = (_b = pkg.version) !== null && _b !== void 0 ? _b : '';
|
|
45
|
+
if (!/^\d+\.\d+\.\d+/.test(version)) {
|
|
46
|
+
throw preconditionError(`package.json "version" is "${version}", not semver.`, 'Set a version such as 0.1.0.');
|
|
47
|
+
}
|
|
48
|
+
const manifest = buildManifest({
|
|
49
|
+
project,
|
|
50
|
+
definition: loaded.definition,
|
|
51
|
+
pinned: result.pinned,
|
|
52
|
+
workflows: result.workflows,
|
|
53
|
+
version,
|
|
54
|
+
sdk: sdkVersion(project, pkg.dependencies),
|
|
55
|
+
});
|
|
56
|
+
const parsed = ExperienceManifestSchema.safeParse(manifest);
|
|
57
|
+
if (!parsed.success) {
|
|
58
|
+
throw preconditionError(`The generated manifest is invalid: ${parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`, 'This is a bug in @ekanos/cli or an unsupported definition; report it.');
|
|
59
|
+
}
|
|
60
|
+
const bundle = await esbuild({
|
|
61
|
+
entryPoints: [path.join(project.dir, project.entry)],
|
|
62
|
+
bundle: true,
|
|
63
|
+
write: false,
|
|
64
|
+
format: 'esm',
|
|
65
|
+
platform: 'browser',
|
|
66
|
+
jsx: 'automatic',
|
|
67
|
+
logLevel: 'silent',
|
|
68
|
+
absWorkingDir: project.dir,
|
|
69
|
+
external: [
|
|
70
|
+
...ALLOWED_EXTERNALS,
|
|
71
|
+
...((_c = project.ekanos.allowImports) !== null && _c !== void 0 ? _c : []),
|
|
72
|
+
].flatMap((n) => [n, `${n}/*`]),
|
|
73
|
+
loader: { '.css': 'empty', '.svg': 'dataurl', '.png': 'dataurl' },
|
|
74
|
+
});
|
|
75
|
+
const js = (_d = (_a = bundle.outputFiles[0]) === null || _a === void 0 ? void 0 : _a.text) !== null && _d !== void 0 ? _d : '';
|
|
76
|
+
const dts = "import type { ExperienceDefinition } from '@ekanos/sdk/experience';\n" +
|
|
77
|
+
'declare const experience: ExperienceDefinition;\n' +
|
|
78
|
+
'export default experience;\n';
|
|
79
|
+
const changed = [
|
|
80
|
+
writeIfChanged(path.join(project.dir, DIST_ENTRY), js),
|
|
81
|
+
writeIfChanged(path.join(project.dir, DIST_TYPES), dts),
|
|
82
|
+
writeIfChanged(path.join(project.dir, MANIFEST_FILE), formatJson(manifest)),
|
|
83
|
+
].some(Boolean);
|
|
84
|
+
return {
|
|
85
|
+
manifest,
|
|
86
|
+
files: [DIST_ENTRY, DIST_TYPES, MANIFEST_FILE],
|
|
87
|
+
changed,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/experience/build.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type ExperienceManifest,\n ExperienceManifestSchema,\n} from '@ekanos/integration-schema';\nimport { build as esbuild } from 'esbuild';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { preconditionError } from '../errors';\nimport { ALLOWED_EXTERNALS } from './boundary';\nimport type { ExperienceCheckResult } from './checks';\nimport { buildManifest } from './definition-checks';\nimport { type ExperienceProject, formatJson, writeIfChanged } from './project';\n\nexport const DIST_ENTRY = 'dist/experience.js';\nexport const DIST_TYPES = 'dist/experience.d.ts';\nexport const MANIFEST_FILE = 'manifest.json';\n\nfunction readPackageJson(project: ExperienceProject): {\n version?: string;\n dependencies?: Record<string, string>;\n} {\n try {\n return JSON.parse(\n fs.readFileSync(path.join(project.dir, 'package.json'), 'utf8'),\n ) as { version?: string; dependencies?: Record<string, string> };\n } catch {\n throw preconditionError(\n 'package.json is missing or unreadable.',\n 'An experience is an npm package; run \"ekanos init --kind experience\" or add a package.json.',\n );\n }\n}\n\nfunction sdkVersion(\n project: ExperienceProject,\n declared: Record<string, string> | undefined,\n): string {\n try {\n const installed = JSON.parse(\n fs.readFileSync(\n path.join(\n project.dir,\n 'node_modules',\n '@ekanos',\n 'sdk',\n 'package.json',\n ),\n 'utf8',\n ),\n ) as { version?: string };\n if (installed.version) return installed.version;\n } catch {\n // not installed: fall back to the declared range\n }\n\n return (declared?.['@ekanos/sdk'] ?? 'unknown').replace(/^[\\^~]/, '');\n}\n\nexport interface BuildOutput {\n manifest: ExperienceManifest;\n files: string[];\n changed: boolean;\n}\n\n/**\n * Emit `dist/experience.js` (browser ESM, runtime packages left external),\n * `dist/experience.d.ts` and `manifest.json`. Assumes the checks passed.\n */\nexport async function emitExperience(\n project: ExperienceProject,\n result: ExperienceCheckResult,\n): Promise<BuildOutput> {\n const loaded = result.loaded;\n if (!loaded) {\n throw preconditionError(\n 'The experience definition was not loaded.',\n 'Run \"ekanos check\" to see why.',\n );\n }\n const pkg = readPackageJson(project);\n const version = pkg.version ?? '';\n if (!/^\\d+\\.\\d+\\.\\d+/.test(version)) {\n throw preconditionError(\n `package.json \"version\" is \"${version}\", not semver.`,\n 'Set a version such as 0.1.0.',\n );\n }\n\n const manifest = buildManifest({\n project,\n definition: loaded.definition,\n pinned: result.pinned,\n workflows: result.workflows,\n version,\n sdk: sdkVersion(project, pkg.dependencies),\n });\n const parsed = ExperienceManifestSchema.safeParse(manifest);\n if (!parsed.success) {\n throw preconditionError(\n `The generated manifest is invalid: ${parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`,\n 'This is a bug in @ekanos/cli or an unsupported definition; report it.',\n );\n }\n\n const bundle = await esbuild({\n entryPoints: [path.join(project.dir, project.entry)],\n bundle: true,\n write: false,\n format: 'esm',\n platform: 'browser',\n jsx: 'automatic',\n logLevel: 'silent',\n absWorkingDir: project.dir,\n external: [\n ...ALLOWED_EXTERNALS,\n ...(project.ekanos.allowImports ?? []),\n ].flatMap((n) => [n, `${n}/*`]),\n loader: { '.css': 'empty', '.svg': 'dataurl', '.png': 'dataurl' },\n });\n const js = bundle.outputFiles[0]?.text ?? '';\n\n const dts =\n \"import type { ExperienceDefinition } from '@ekanos/sdk/experience';\\n\" +\n 'declare const experience: ExperienceDefinition;\\n' +\n 'export default experience;\\n';\n\n const changed = [\n writeIfChanged(path.join(project.dir, DIST_ENTRY), js),\n writeIfChanged(path.join(project.dir, DIST_TYPES), dts),\n writeIfChanged(path.join(project.dir, MANIFEST_FILE), formatJson(manifest)),\n ].some(Boolean);\n\n return {\n manifest,\n files: [DIST_ENTRY, DIST_TYPES, MANIFEST_FILE],\n changed,\n };\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Finding, WorkflowTemplate } from '@ekanos/integration-schema';
|
|
2
|
+
import type { LoadedExperienceModule } from './definition-shape.js';
|
|
3
|
+
import { type PinnedIntegration, type PinsResolver } from './pins.js';
|
|
4
|
+
import { type ExperienceProject } from './project.js';
|
|
5
|
+
export declare const GENERATED_TYPES_CHECK = "checks/generated-types";
|
|
6
|
+
export declare const LOCAL_OVERRIDES_CHECK = "checks/local-overrides";
|
|
7
|
+
export declare const TYPECHECK_CHECK = "checks/typecheck";
|
|
8
|
+
export interface ExperienceCheckOptions {
|
|
9
|
+
typecheck: boolean;
|
|
10
|
+
/** Skip loading the entry (used by commands that only need the static stages). */
|
|
11
|
+
loadDefinition: boolean;
|
|
12
|
+
inlineSdk?: string;
|
|
13
|
+
/** The harness's pins resolver (`harnessPinsResolver`); absent = committed snapshots only. */
|
|
14
|
+
pinsResolver?: PinsResolver | null;
|
|
15
|
+
}
|
|
16
|
+
export interface ExperienceCheckResult {
|
|
17
|
+
findings: Finding[];
|
|
18
|
+
pinned: PinnedIntegration[];
|
|
19
|
+
loaded?: LoadedExperienceModule;
|
|
20
|
+
workflows: WorkflowTemplate[];
|
|
21
|
+
}
|
|
22
|
+
/** The dts the pinned contracts generate: shared by `codegen` and the freshness check. */
|
|
23
|
+
export declare function expectedIntegrationsDts(pinned: readonly PinnedIntegration[]): string;
|
|
24
|
+
/**
|
|
25
|
+
* The experience gate, in dependency order. Stages that need an earlier stage's
|
|
26
|
+
* output are skipped when it failed, so one root cause yields one finding, not
|
|
27
|
+
* a cascade.
|
|
28
|
+
*/
|
|
29
|
+
export declare function runExperienceChecks(project: ExperienceProject, options: ExperienceCheckOptions): Promise<ExperienceCheckResult>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { checkExperienceBoundary } from './boundary.js';
|
|
5
|
+
import { analyzeDefinition, checkFixtures, } from './definition-checks.js';
|
|
6
|
+
import { generateIntegrationsDts } from './integrations-codegen.js';
|
|
7
|
+
import { loadExperience } from './load-experience.js';
|
|
8
|
+
import { isOverridesFileTracked } from './overrides.js';
|
|
9
|
+
import { checkIntegrationPins, } from './pins.js';
|
|
10
|
+
import { LOCAL_OVERRIDES_FILE, INTEGRATIONS_DTS, LEGACY_SLOTS_DTS, readLockfile, } from './project.js';
|
|
11
|
+
export const GENERATED_TYPES_CHECK = 'checks/generated-types';
|
|
12
|
+
export const LOCAL_OVERRIDES_CHECK = 'checks/local-overrides';
|
|
13
|
+
export const TYPECHECK_CHECK = 'checks/typecheck';
|
|
14
|
+
/** The dts the pinned contracts generate: shared by `codegen` and the freshness check. */
|
|
15
|
+
export function expectedIntegrationsDts(pinned) {
|
|
16
|
+
return generateIntegrationsDts(pinned.map(({ use, contract }) => ({
|
|
17
|
+
integration: use.name,
|
|
18
|
+
contract,
|
|
19
|
+
operations: use.operations
|
|
20
|
+
.map((id) => { var _a; return (_a = /\/((?:tool|action)\/.+)$/.exec(id)) === null || _a === void 0 ? void 0 : _a[1]; })
|
|
21
|
+
.filter((key) => key !== undefined),
|
|
22
|
+
})));
|
|
23
|
+
}
|
|
24
|
+
function checkGeneratedTypes(project, pinned) {
|
|
25
|
+
const file = path.join(project.dir, INTEGRATIONS_DTS);
|
|
26
|
+
const current = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
|
27
|
+
const findings = [];
|
|
28
|
+
if (fs.existsSync(path.join(project.dir, LEGACY_SLOTS_DTS))) {
|
|
29
|
+
findings.push({
|
|
30
|
+
check: GENERATED_TYPES_CHECK,
|
|
31
|
+
severity: 'warn',
|
|
32
|
+
message: `${LEGACY_SLOTS_DTS} is the previous name of ${INTEGRATIONS_DTS}.`,
|
|
33
|
+
hint: `Delete it and list ${INTEGRATIONS_DTS} in tsconfig.json "include"; until then "ekanos sync" keeps it as a re-export.`,
|
|
34
|
+
file: LEGACY_SLOTS_DTS,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (current === expectedIntegrationsDts(pinned))
|
|
38
|
+
return findings;
|
|
39
|
+
return [
|
|
40
|
+
...findings,
|
|
41
|
+
{
|
|
42
|
+
check: GENERATED_TYPES_CHECK,
|
|
43
|
+
severity: 'error',
|
|
44
|
+
message: `${INTEGRATIONS_DTS} is ${current === null ? 'missing' : 'out of date'} for the pinned contracts.`,
|
|
45
|
+
hint: 'Run "ekanos codegen" and commit the result.',
|
|
46
|
+
file: INTEGRATIONS_DTS,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
function runTypecheck(project) {
|
|
51
|
+
const tsc = path.join(project.dir, 'node_modules', '.bin', 'tsc');
|
|
52
|
+
if (!fs.existsSync(tsc) ||
|
|
53
|
+
!fs.existsSync(path.join(project.dir, 'tsconfig.json'))) {
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
check: TYPECHECK_CHECK,
|
|
57
|
+
severity: 'warn',
|
|
58
|
+
message: 'typecheck skipped: no local TypeScript or tsconfig.json.',
|
|
59
|
+
hint: 'Install typescript and keep a tsconfig.json to type-check against the generated integrations.',
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
const result = spawnSync(tsc, ['--noEmit', '-p', 'tsconfig.json'], {
|
|
64
|
+
cwd: project.dir,
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
});
|
|
67
|
+
if (result.status === 0)
|
|
68
|
+
return [];
|
|
69
|
+
const lines = `${result.stdout}${result.stderr}`
|
|
70
|
+
.split('\n')
|
|
71
|
+
.filter((l) => l.trim() !== '');
|
|
72
|
+
return lines.slice(0, 20).map((line) => {
|
|
73
|
+
const m = /^(.+?)\((\d+),\d+\): (.*)$/.exec(line);
|
|
74
|
+
return Object.assign({ check: TYPECHECK_CHECK, severity: 'error', message: m ? m[3] : line, hint: 'Fix the type error; it is what the generated integration types caught.' }, (m ? { file: m[1], line: Number(m[2]) } : {}));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The experience gate, in dependency order. Stages that need an earlier stage's
|
|
79
|
+
* output are skipped when it failed, so one root cause yields one finding, not
|
|
80
|
+
* a cascade.
|
|
81
|
+
*/
|
|
82
|
+
export async function runExperienceChecks(project, options) {
|
|
83
|
+
var _a, _b;
|
|
84
|
+
const findings = [];
|
|
85
|
+
const lock = readLockfile(project);
|
|
86
|
+
const pins = checkIntegrationPins(project, lock, (_a = options.pinsResolver) !== null && _a !== void 0 ? _a : undefined);
|
|
87
|
+
findings.push(...pins.findings);
|
|
88
|
+
if (isOverridesFileTracked(project)) {
|
|
89
|
+
findings.push({
|
|
90
|
+
check: LOCAL_OVERRIDES_CHECK,
|
|
91
|
+
severity: 'error',
|
|
92
|
+
message: `${LOCAL_OVERRIDES_FILE} is tracked by git.`,
|
|
93
|
+
hint: `It is per-developer and must stay untracked: "git rm --cached ${LOCAL_OVERRIDES_FILE}" and add it to .gitignore.`,
|
|
94
|
+
file: LOCAL_OVERRIDES_FILE,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const pinsOk = pins.findings.length === 0;
|
|
98
|
+
if (pinsOk || pins.pinned.length > 0) {
|
|
99
|
+
findings.push(...checkGeneratedTypes(project, pins.pinned));
|
|
100
|
+
}
|
|
101
|
+
findings.push(...(await checkExperienceBoundary(project)));
|
|
102
|
+
findings.push(...checkFixtures(project, pins.pinned));
|
|
103
|
+
let loaded;
|
|
104
|
+
let analysis;
|
|
105
|
+
if (options.loadDefinition && pinsOk) {
|
|
106
|
+
const result = await loadExperience(project, {
|
|
107
|
+
inlineSdk: options.inlineSdk,
|
|
108
|
+
});
|
|
109
|
+
if (result.ok) {
|
|
110
|
+
loaded = result;
|
|
111
|
+
analysis = analyzeDefinition(project, result, pins.pinned);
|
|
112
|
+
findings.push(...analysis.findings);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
findings.push({
|
|
116
|
+
check: 'checks/experience-definition',
|
|
117
|
+
severity: 'error',
|
|
118
|
+
message: result.message,
|
|
119
|
+
hint: 'Fix the entry so it exports defineExperience({...}) as default.',
|
|
120
|
+
file: project.entry,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (options.typecheck)
|
|
125
|
+
findings.push(...runTypecheck(project));
|
|
126
|
+
return {
|
|
127
|
+
findings,
|
|
128
|
+
pinned: pins.pinned,
|
|
129
|
+
loaded,
|
|
130
|
+
workflows: (_b = analysis === null || analysis === void 0 ? void 0 : analysis.workflows) !== null && _b !== void 0 ? _b : [],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=checks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checks.js","sourceRoot":"","sources":["../../src/experience/checks.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Finding, WorkflowTemplate } from '@ekanos/integration-schema';\nimport { spawnSync } from 'node:child_process';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { checkExperienceBoundary } from './boundary';\nimport {\n type DefinitionAnalysis,\n analyzeDefinition,\n checkFixtures,\n} from './definition-checks';\nimport type { LoadedExperienceModule } from './definition-shape';\nimport { generateIntegrationsDts } from './integrations-codegen';\nimport { loadExperience } from './load-experience';\nimport { isOverridesFileTracked } from './overrides';\nimport {\n type PinnedIntegration,\n type PinsResolver,\n checkIntegrationPins,\n} from './pins';\nimport {\n LOCAL_OVERRIDES_FILE,\n type ExperienceProject,\n INTEGRATIONS_DTS,\n LEGACY_SLOTS_DTS,\n readLockfile,\n} from './project';\n\nexport const GENERATED_TYPES_CHECK = 'checks/generated-types';\nexport const LOCAL_OVERRIDES_CHECK = 'checks/local-overrides';\nexport const TYPECHECK_CHECK = 'checks/typecheck';\n\nexport interface ExperienceCheckOptions {\n typecheck: boolean;\n /** Skip loading the entry (used by commands that only need the static stages). */\n loadDefinition: boolean;\n inlineSdk?: string;\n /** The harness's pins resolver (`harnessPinsResolver`); absent = committed snapshots only. */\n pinsResolver?: PinsResolver | null;\n}\n\nexport interface ExperienceCheckResult {\n findings: Finding[];\n pinned: PinnedIntegration[];\n loaded?: LoadedExperienceModule;\n workflows: WorkflowTemplate[];\n}\n\n/** The dts the pinned contracts generate: shared by `codegen` and the freshness check. */\nexport function expectedIntegrationsDts(\n pinned: readonly PinnedIntegration[],\n): string {\n return generateIntegrationsDts(\n pinned.map(({ use, contract }) => ({\n integration: use.name,\n contract,\n operations: use.operations\n .map((id) => /\\/((?:tool|action)\\/.+)$/.exec(id)?.[1])\n .filter((key): key is string => key !== undefined),\n })),\n );\n}\n\nfunction checkGeneratedTypes(\n project: ExperienceProject,\n pinned: readonly PinnedIntegration[],\n): Finding[] {\n const file = path.join(project.dir, INTEGRATIONS_DTS);\n const current = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;\n const findings: Finding[] = [];\n if (fs.existsSync(path.join(project.dir, LEGACY_SLOTS_DTS))) {\n findings.push({\n check: GENERATED_TYPES_CHECK,\n severity: 'warn',\n message: `${LEGACY_SLOTS_DTS} is the previous name of ${INTEGRATIONS_DTS}.`,\n hint: `Delete it and list ${INTEGRATIONS_DTS} in tsconfig.json \"include\"; until then \"ekanos sync\" keeps it as a re-export.`,\n file: LEGACY_SLOTS_DTS,\n });\n }\n if (current === expectedIntegrationsDts(pinned)) return findings;\n\n return [\n ...findings,\n {\n check: GENERATED_TYPES_CHECK,\n severity: 'error',\n message: `${INTEGRATIONS_DTS} is ${current === null ? 'missing' : 'out of date'} for the pinned contracts.`,\n hint: 'Run \"ekanos codegen\" and commit the result.',\n file: INTEGRATIONS_DTS,\n },\n ];\n}\n\nfunction runTypecheck(project: ExperienceProject): Finding[] {\n const tsc = path.join(project.dir, 'node_modules', '.bin', 'tsc');\n if (\n !fs.existsSync(tsc) ||\n !fs.existsSync(path.join(project.dir, 'tsconfig.json'))\n ) {\n return [\n {\n check: TYPECHECK_CHECK,\n severity: 'warn',\n message: 'typecheck skipped: no local TypeScript or tsconfig.json.',\n hint: 'Install typescript and keep a tsconfig.json to type-check against the generated integrations.',\n },\n ];\n }\n const result = spawnSync(tsc, ['--noEmit', '-p', 'tsconfig.json'], {\n cwd: project.dir,\n encoding: 'utf8',\n });\n if (result.status === 0) return [];\n\n const lines = `${result.stdout}${result.stderr}`\n .split('\\n')\n .filter((l) => l.trim() !== '');\n\n return lines.slice(0, 20).map((line) => {\n const m = /^(.+?)\\((\\d+),\\d+\\): (.*)$/.exec(line);\n\n return {\n check: TYPECHECK_CHECK,\n severity: 'error' as const,\n message: m ? (m[3] as string) : line,\n hint: 'Fix the type error; it is what the generated integration types caught.',\n ...(m ? { file: m[1] as string, line: Number(m[2]) } : {}),\n };\n });\n}\n\n/**\n * The experience gate, in dependency order. Stages that need an earlier stage's\n * output are skipped when it failed, so one root cause yields one finding, not\n * a cascade.\n */\nexport async function runExperienceChecks(\n project: ExperienceProject,\n options: ExperienceCheckOptions,\n): Promise<ExperienceCheckResult> {\n const findings: Finding[] = [];\n const lock = readLockfile(project);\n\n const pins = checkIntegrationPins(\n project,\n lock,\n options.pinsResolver ?? undefined,\n );\n findings.push(...pins.findings);\n\n if (isOverridesFileTracked(project)) {\n findings.push({\n check: LOCAL_OVERRIDES_CHECK,\n severity: 'error',\n message: `${LOCAL_OVERRIDES_FILE} is tracked by git.`,\n hint: `It is per-developer and must stay untracked: \"git rm --cached ${LOCAL_OVERRIDES_FILE}\" and add it to .gitignore.`,\n file: LOCAL_OVERRIDES_FILE,\n });\n }\n\n const pinsOk = pins.findings.length === 0;\n if (pinsOk || pins.pinned.length > 0) {\n findings.push(...checkGeneratedTypes(project, pins.pinned));\n }\n\n findings.push(...(await checkExperienceBoundary(project)));\n findings.push(...checkFixtures(project, pins.pinned));\n\n let loaded: LoadedExperienceModule | undefined;\n let analysis: DefinitionAnalysis | undefined;\n if (options.loadDefinition && pinsOk) {\n const result = await loadExperience(project, {\n inlineSdk: options.inlineSdk,\n });\n if (result.ok) {\n loaded = result;\n analysis = analyzeDefinition(project, result, pins.pinned);\n findings.push(...analysis.findings);\n } else {\n findings.push({\n check: 'checks/experience-definition',\n severity: 'error',\n message: result.message,\n hint: 'Fix the entry so it exports defineExperience({...}) as default.',\n file: project.entry,\n });\n }\n }\n\n if (options.typecheck) findings.push(...runTypecheck(project));\n\n return {\n findings,\n pinned: pins.pinned,\n loaded,\n workflows: analysis?.workflows ?? [],\n };\n}\n"]}
|