@elaraai/create-e3 1.0.30 → 1.0.31
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/dist/index.js +273 -31
- package/package.json +1 -1
- package/templates/e3/_packages/c/Makefile +16 -0
- package/templates/e3/_packages/c/_app.ts +22 -0
- package/templates/e3/_packages/c/src/__PACKAGE_NAME__.c +36 -0
- package/templates/e3/_packages/node/_app.ts +25 -0
- package/templates/e3/_packages/node/package.json +24 -0
- package/templates/e3/_packages/node/src/platform.ts +17 -0
- package/templates/e3/_packages/node/tsconfig.json +23 -0
- package/templates/e3/_packages/python/_app.ts +26 -0
- package/templates/e3/_packages/python/pyproject.toml +16 -0
- package/templates/e3/_packages/python/src/__PACKAGE_NAME__/__init__.py +18 -0
- package/templates/e3/_packages/python/src/__PACKAGE_NAME__/example.py +27 -0
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// ../scaffold-core/dist/scaffold.js
|
|
4
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { join, relative } from "node:path";
|
|
4
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5
|
+
import { join as join2, relative as relative2 } from "node:path";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
|
|
8
8
|
// ../scaffold-core/dist/names.js
|
|
@@ -18,6 +18,203 @@ function deriveNames(rawName, cwd) {
|
|
|
18
18
|
return { projectName, displayName, workspaceName };
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// ../scaffold-core/dist/packages.js
|
|
22
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { join, relative } from "node:path";
|
|
24
|
+
var RUNTIMES = {
|
|
25
|
+
// Python package/dir names must be import-safe (letters, digits, underscore).
|
|
26
|
+
python: {
|
|
27
|
+
runtime: "python",
|
|
28
|
+
destParent: "packages/python",
|
|
29
|
+
templateName: "python",
|
|
30
|
+
label: "Python",
|
|
31
|
+
valid: /^[a-z][a-z0-9_]*$/,
|
|
32
|
+
rule: "lowercase letters, digits and underscores, starting with a letter"
|
|
33
|
+
},
|
|
34
|
+
// Node member names become the last segment of an npm dir; keep them tame.
|
|
35
|
+
node: {
|
|
36
|
+
runtime: "node",
|
|
37
|
+
destParent: "packages/node",
|
|
38
|
+
templateName: "node",
|
|
39
|
+
label: "Node",
|
|
40
|
+
valid: /^[a-z][a-z0-9-]*$/,
|
|
41
|
+
rule: "lowercase letters, digits and hyphens, starting with a letter"
|
|
42
|
+
},
|
|
43
|
+
// C build dir names — letters, digits, underscore, hyphen.
|
|
44
|
+
c: {
|
|
45
|
+
runtime: "c",
|
|
46
|
+
destParent: "packages/native",
|
|
47
|
+
templateName: "c",
|
|
48
|
+
label: "C",
|
|
49
|
+
valid: /^[a-z][a-z0-9_-]*$/,
|
|
50
|
+
rule: "lowercase letters, digits, underscores and hyphens, starting with a letter"
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var RUNTIME_ORDER = ["python", "node", "c"];
|
|
54
|
+
function toIdent(name) {
|
|
55
|
+
return name.replace(/[^a-zA-Z0-9]/g, "_");
|
|
56
|
+
}
|
|
57
|
+
function substitute(text, names, version, pkg) {
|
|
58
|
+
let out = text.replaceAll("__PROJECT_NAME__", names.projectName).replaceAll("__DISPLAY_NAME__", names.displayName).replaceAll("__WORKSPACE_NAME__", names.workspaceName).replaceAll("__VERSION__", version);
|
|
59
|
+
if (pkg)
|
|
60
|
+
out = out.replaceAll("__PACKAGE_NAME__", pkg.name).replaceAll("__PACKAGE_IDENT__", pkg.ident);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function walkFiles(dir) {
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const entry of readdirSync(dir)) {
|
|
66
|
+
const full = join(dir, entry);
|
|
67
|
+
if (statSync(full).isDirectory())
|
|
68
|
+
out.push(...walkFiles(full));
|
|
69
|
+
else
|
|
70
|
+
out.push(full);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function stampMember(projectDir, templateRoot, cfg, names, version, pkg) {
|
|
75
|
+
const memberTemplate = join(templateRoot, cfg.templateName);
|
|
76
|
+
if (!existsSync(memberTemplate)) {
|
|
77
|
+
throw new Error(`internal: missing package template for ${cfg.label} at ${memberTemplate}`);
|
|
78
|
+
}
|
|
79
|
+
const memberDir = join(projectDir, cfg.destParent, pkg.name);
|
|
80
|
+
for (const srcPath of walkFiles(memberTemplate)) {
|
|
81
|
+
const rel = relative(memberTemplate, srcPath).replaceAll("\\", "/");
|
|
82
|
+
if (!rel.includes("/") && rel.startsWith("_")) {
|
|
83
|
+
if (rel === "_app.ts") {
|
|
84
|
+
const dest2 = join(projectDir, "src", "packages", `${pkg.name}.ts`);
|
|
85
|
+
mkdirSync(join(dest2, ".."), { recursive: true });
|
|
86
|
+
writeFileSync(dest2, substitute(readFileSync(srcPath, "utf8"), names, version, pkg));
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const dest = join(memberDir, substitute(rel, names, version, pkg));
|
|
91
|
+
mkdirSync(join(dest, ".."), { recursive: true });
|
|
92
|
+
writeFileSync(dest, substitute(readFileSync(srcPath, "utf8"), names, version, pkg));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function writePythonWorkspaceRoot(projectDir, names, members) {
|
|
96
|
+
const sources = members.map((m) => `${m} = { workspace = true }`).join("\n");
|
|
97
|
+
const body = `[project]
|
|
98
|
+
name = "${names.projectName}"
|
|
99
|
+
version = "0.1.0"
|
|
100
|
+
description = "${names.displayName}"
|
|
101
|
+
requires-python = ">=3.11"
|
|
102
|
+
dependencies = [
|
|
103
|
+
"elaraai-east-py",
|
|
104
|
+
"elaraai-east-py-std",
|
|
105
|
+
"elaraai-east-py-io",
|
|
106
|
+
"elaraai-east-py-datascience",
|
|
107
|
+
"elaraai-east-py-cli",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
# This project is a uv workspace root: each package under packages/python/*
|
|
111
|
+
# is its own member with its own dependency closure (and its own captured
|
|
112
|
+
# execution environment). No [build-system] \u2014 the root is not itself a
|
|
113
|
+
# Python package, just the workspace + the shared runtime dependencies.
|
|
114
|
+
[tool.uv.workspace]
|
|
115
|
+
members = ["packages/python/*"]
|
|
116
|
+
|
|
117
|
+
[tool.uv.sources]
|
|
118
|
+
${sources}
|
|
119
|
+
`;
|
|
120
|
+
writeFileSync(join(projectDir, "pyproject.toml"), body);
|
|
121
|
+
}
|
|
122
|
+
function patchNodeWorkspaceRoot(projectDir) {
|
|
123
|
+
const path = join(projectDir, "package.json");
|
|
124
|
+
const pkg = JSON.parse(readFileSync(path, "utf8"));
|
|
125
|
+
const globs = new Set(Array.isArray(pkg.workspaces) ? pkg.workspaces : []);
|
|
126
|
+
globs.add("packages/node/*");
|
|
127
|
+
pkg.workspaces = [...globs];
|
|
128
|
+
pkg.private = true;
|
|
129
|
+
const scripts = pkg.scripts ?? {};
|
|
130
|
+
if (typeof scripts.build === "string" && !scripts.build.includes("--workspaces")) {
|
|
131
|
+
scripts.build = `npm run build --workspaces --if-present && ${scripts.build}`;
|
|
132
|
+
pkg.scripts = scripts;
|
|
133
|
+
}
|
|
134
|
+
writeFileSync(path, `${JSON.stringify(pkg, null, 2)}
|
|
135
|
+
`);
|
|
136
|
+
}
|
|
137
|
+
function excludePackagesFromRootTsconfig(projectDir) {
|
|
138
|
+
const path = join(projectDir, "tsconfig.json");
|
|
139
|
+
if (!existsSync(path))
|
|
140
|
+
return;
|
|
141
|
+
const tsconfig = JSON.parse(readFileSync(path, "utf8"));
|
|
142
|
+
const exclude = new Set(tsconfig.exclude ?? []);
|
|
143
|
+
exclude.add("packages");
|
|
144
|
+
tsconfig.exclude = [...exclude];
|
|
145
|
+
writeFileSync(path, `${JSON.stringify(tsconfig, null, 2)}
|
|
146
|
+
`);
|
|
147
|
+
}
|
|
148
|
+
function writeAppWiring(projectDir, names, members) {
|
|
149
|
+
const imports = members.map((m) => `import { ${m.ident}_task } from "./${m.name}.js";`).join("\n");
|
|
150
|
+
const list = members.map((m) => ` ${m.ident}_task,`).join("\n");
|
|
151
|
+
const barrel = `// Generated by create-e3 \u2014 collects one example task per workspace package.
|
|
152
|
+
// Each task's \`environment\` points at its package, so editing one package
|
|
153
|
+
// re-runs only its task. Add packages with \`create-e3 --python-packages=\u2026\`.
|
|
154
|
+
${imports}
|
|
155
|
+
|
|
156
|
+
export const packageTasks = [
|
|
157
|
+
${list}
|
|
158
|
+
];
|
|
159
|
+
`;
|
|
160
|
+
mkdirSync(join(projectDir, "src", "packages"), { recursive: true });
|
|
161
|
+
writeFileSync(join(projectDir, "src", "packages", "index.ts"), barrel);
|
|
162
|
+
const index = `import e3 from "@elaraai/e3";
|
|
163
|
+
|
|
164
|
+
import { packageTasks } from "./packages/index.js";
|
|
165
|
+
|
|
166
|
+
// This app wires one example task per generated workspace package. Every
|
|
167
|
+
// task's \`environment\` is captured independently, so editing one package
|
|
168
|
+
// re-runs only its task \u2014 the other packages stay cached. Add your own
|
|
169
|
+
// inputs, tasks and dataflows here.
|
|
170
|
+
export default e3.package("${names.projectName}", "1.0.0", ...packageTasks);
|
|
171
|
+
`;
|
|
172
|
+
writeFileSync(join(projectDir, "src", "index.ts"), index);
|
|
173
|
+
}
|
|
174
|
+
function generatePackages(options) {
|
|
175
|
+
const { projectDir, templateDir, names, version, spec, log } = options;
|
|
176
|
+
const templateRoot = join(templateDir, "_packages");
|
|
177
|
+
const members = [];
|
|
178
|
+
const seen = /* @__PURE__ */ new Map();
|
|
179
|
+
for (const runtime of RUNTIME_ORDER) {
|
|
180
|
+
const cfg = RUNTIMES[runtime];
|
|
181
|
+
for (const raw of spec[runtime] ?? []) {
|
|
182
|
+
const name = raw.trim();
|
|
183
|
+
if (!name)
|
|
184
|
+
throw new Error(`--${runtime}-packages contains an empty package name`);
|
|
185
|
+
if (!cfg.valid.test(name)) {
|
|
186
|
+
throw new Error(`${cfg.label} package name '${name}' is invalid \u2014 use ${cfg.rule}`);
|
|
187
|
+
}
|
|
188
|
+
const prior = seen.get(name);
|
|
189
|
+
if (prior) {
|
|
190
|
+
throw new Error(`package name '${name}' is declared twice (${prior} and ${runtime}) \u2014 names must be unique`);
|
|
191
|
+
}
|
|
192
|
+
seen.set(name, runtime);
|
|
193
|
+
members.push({ name, ident: toIdent(name), runtime });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (members.length === 0)
|
|
197
|
+
return;
|
|
198
|
+
for (const runtime of RUNTIME_ORDER) {
|
|
199
|
+
if ((spec[runtime]?.length ?? 0) > 0 && !existsSync(join(templateRoot, RUNTIMES[runtime].templateName))) {
|
|
200
|
+
throw new Error(`${RUNTIMES[runtime].label} packages are not available in this scaffold yet \u2014 remove --${runtime}-packages for now`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const pkg of members) {
|
|
204
|
+
stampMember(projectDir, templateRoot, RUNTIMES[pkg.runtime], names, version, pkg);
|
|
205
|
+
}
|
|
206
|
+
const py = members.filter((m) => m.runtime === "python").map((m) => m.name);
|
|
207
|
+
const node = members.filter((m) => m.runtime === "node").map((m) => m.name);
|
|
208
|
+
if (py.length > 0)
|
|
209
|
+
writePythonWorkspaceRoot(projectDir, names, py);
|
|
210
|
+
if (node.length > 0)
|
|
211
|
+
patchNodeWorkspaceRoot(projectDir);
|
|
212
|
+
excludePackagesFromRootTsconfig(projectDir);
|
|
213
|
+
writeAppWiring(projectDir, names, members);
|
|
214
|
+
const summary = RUNTIME_ORDER.map((r) => ({ r, n: members.filter((m) => m.runtime === r).length })).filter((x) => x.n > 0).map((x) => `${x.n} ${RUNTIMES[x.r].label}`).join(", ");
|
|
215
|
+
log(`Generated ${members.length} workspace package${members.length === 1 ? "" : "s"} (${summary}).`);
|
|
216
|
+
}
|
|
217
|
+
|
|
21
218
|
// ../scaffold-core/dist/scaffold.js
|
|
22
219
|
var TEXT_REPLACE_EXT = /* @__PURE__ */ new Set([
|
|
23
220
|
".ts",
|
|
@@ -102,9 +299,9 @@ function transformPackageJson(raw, names, version, manifest, enabled) {
|
|
|
102
299
|
}
|
|
103
300
|
function walk(dir) {
|
|
104
301
|
const out = [];
|
|
105
|
-
for (const entry of
|
|
106
|
-
const full =
|
|
107
|
-
if (
|
|
302
|
+
for (const entry of readdirSync2(dir)) {
|
|
303
|
+
const full = join2(dir, entry);
|
|
304
|
+
if (statSync2(full).isDirectory())
|
|
108
305
|
out.push(...walk(full));
|
|
109
306
|
else
|
|
110
307
|
out.push(full);
|
|
@@ -117,16 +314,16 @@ function extOf(path) {
|
|
|
117
314
|
return dot <= 0 ? "" : base.slice(dot);
|
|
118
315
|
}
|
|
119
316
|
function loadManifest(templateDir) {
|
|
120
|
-
const path =
|
|
121
|
-
if (!
|
|
317
|
+
const path = join2(templateDir, MANIFEST_FILE);
|
|
318
|
+
if (!existsSync2(path))
|
|
122
319
|
return null;
|
|
123
|
-
return JSON.parse(
|
|
320
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
124
321
|
}
|
|
125
322
|
function scaffold(options) {
|
|
126
323
|
const { kind, name, templateDir, version } = options;
|
|
127
324
|
const cwd = options.cwd ?? process.cwd();
|
|
128
325
|
const log = options.log ?? ((m) => console.log(m));
|
|
129
|
-
if (!
|
|
326
|
+
if (!existsSync2(templateDir)) {
|
|
130
327
|
throw new Error(`Template directory not found: ${templateDir}`);
|
|
131
328
|
}
|
|
132
329
|
const manifest = loadManifest(templateDir);
|
|
@@ -164,34 +361,39 @@ function scaffold(options) {
|
|
|
164
361
|
}
|
|
165
362
|
const names = deriveNames(name, cwd);
|
|
166
363
|
const inPlace = name === ".";
|
|
167
|
-
const projectDir = inPlace ? cwd :
|
|
168
|
-
if (!inPlace &&
|
|
364
|
+
const projectDir = inPlace ? cwd : join2(cwd, names.projectName);
|
|
365
|
+
if (!inPlace && existsSync2(projectDir)) {
|
|
169
366
|
throw new Error(`Directory already exists: ${projectDir}`);
|
|
170
367
|
}
|
|
171
|
-
|
|
368
|
+
mkdirSync2(projectDir, { recursive: true });
|
|
172
369
|
for (const srcPath of walk(templateDir)) {
|
|
173
|
-
const rel =
|
|
370
|
+
const rel = relative2(templateDir, srcPath).replaceAll("\\", "/");
|
|
174
371
|
if (rel === MANIFEST_FILE)
|
|
175
372
|
continue;
|
|
373
|
+
if (rel.split("/")[0].startsWith("_"))
|
|
374
|
+
continue;
|
|
176
375
|
if (skip.has(rel))
|
|
177
376
|
continue;
|
|
178
377
|
const destRel = renames[rel] ?? rel;
|
|
179
378
|
const segments = destRel.split("/");
|
|
180
379
|
const last = segments[segments.length - 1];
|
|
181
380
|
segments[segments.length - 1] = DOTFILE_RENAMES[last] ?? last;
|
|
182
|
-
const destPath =
|
|
183
|
-
|
|
381
|
+
const destPath = join2(projectDir, segments.join("/"));
|
|
382
|
+
mkdirSync2(join2(destPath, ".."), { recursive: true });
|
|
184
383
|
const baseName = segments[segments.length - 1];
|
|
185
384
|
if (baseName === "package.json") {
|
|
186
|
-
const transformed = transformPackageJson(
|
|
187
|
-
|
|
385
|
+
const transformed = transformPackageJson(readFileSync2(srcPath, "utf8"), names, version, manifest, enabled);
|
|
386
|
+
writeFileSync2(destPath, substituteTokens(transformed, names));
|
|
188
387
|
} else if (TEXT_REPLACE_EXT.has(extOf(srcPath)) || baseName.startsWith(".")) {
|
|
189
|
-
|
|
388
|
+
writeFileSync2(destPath, substituteTokens(readFileSync2(srcPath, "utf8"), names));
|
|
190
389
|
} else {
|
|
191
|
-
|
|
390
|
+
writeFileSync2(destPath, readFileSync2(srcPath));
|
|
192
391
|
}
|
|
193
392
|
}
|
|
194
393
|
log(`Created ${names.projectName} (${kind}) at ${projectDir}`);
|
|
394
|
+
if (options.packages) {
|
|
395
|
+
generatePackages({ projectDir, templateDir, names, version, spec: options.packages, log });
|
|
396
|
+
}
|
|
195
397
|
if (options.install) {
|
|
196
398
|
runInstall(kind, projectDir, enabled, log);
|
|
197
399
|
}
|
|
@@ -221,35 +423,36 @@ function runInstall(kind, projectDir, enabled, log) {
|
|
|
221
423
|
}
|
|
222
424
|
|
|
223
425
|
// ../scaffold-core/dist/cli.js
|
|
224
|
-
import { existsSync as
|
|
225
|
-
import { dirname, join as
|
|
426
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
427
|
+
import { dirname, join as join3 } from "node:path";
|
|
226
428
|
import { createInterface } from "node:readline/promises";
|
|
227
429
|
import { fileURLToPath } from "node:url";
|
|
228
430
|
async function runCreateCli(kind, moduleUrl) {
|
|
229
|
-
const pkgRoot =
|
|
431
|
+
const pkgRoot = join3(dirname(fileURLToPath(moduleUrl)), "..");
|
|
230
432
|
const args = process.argv.slice(2);
|
|
231
433
|
if (args.includes("--help") || args.includes("-h")) {
|
|
232
434
|
printHelp(kind);
|
|
233
435
|
return;
|
|
234
436
|
}
|
|
235
|
-
const version = JSON.parse(
|
|
236
|
-
const templateDir =
|
|
437
|
+
const version = JSON.parse(readFileSync3(join3(pkgRoot, "package.json"), "utf8")).version;
|
|
438
|
+
const templateDir = join3(pkgRoot, "templates", kind);
|
|
237
439
|
const name = args.find((a) => !a.startsWith("-")) ?? ".";
|
|
238
440
|
const install = args.includes("--install") ? true : args.includes("--no-install") ? false : Boolean(process.stdout.isTTY);
|
|
239
441
|
try {
|
|
240
|
-
const
|
|
241
|
-
const
|
|
442
|
+
const packages = kind === "e3" ? parsePackages(args) : void 0;
|
|
443
|
+
const features = await resolveFeatures(templateDir, args, packages);
|
|
444
|
+
const result = scaffold({ kind, name, templateDir, version, install, features, packages });
|
|
242
445
|
printNextSteps(kind, result.projectName, result.inPlace, install, features);
|
|
243
446
|
} catch (err) {
|
|
244
447
|
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
245
448
|
process.exit(1);
|
|
246
449
|
}
|
|
247
450
|
}
|
|
248
|
-
async function resolveFeatures(templateDir, args) {
|
|
249
|
-
const manifestPath =
|
|
250
|
-
if (!
|
|
451
|
+
async function resolveFeatures(templateDir, args, packages) {
|
|
452
|
+
const manifestPath = join3(templateDir, "template.json");
|
|
453
|
+
if (!existsSync3(manifestPath))
|
|
251
454
|
return {};
|
|
252
|
-
const manifest = JSON.parse(
|
|
455
|
+
const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
253
456
|
const features = {};
|
|
254
457
|
for (const [key, spec] of Object.entries(manifest.features)) {
|
|
255
458
|
if (spec.allOf)
|
|
@@ -259,7 +462,9 @@ async function resolveFeatures(templateDir, args) {
|
|
|
259
462
|
const runnerKeys = Object.keys(manifest.features).filter((k) => k.startsWith("runner:"));
|
|
260
463
|
const runnerName = (key) => key.slice("runner:".length);
|
|
261
464
|
const runnersFlag = args.find((a) => a.startsWith("--runners="));
|
|
262
|
-
const selectionFlags = ["--tests", "--no-tests", "--ui", "--no-ui", "--platform", "--no-platform", "--eslint", "--no-eslint"].some((f) => args.includes(f)) || Boolean(runnersFlag)
|
|
465
|
+
const selectionFlags = ["--tests", "--no-tests", "--ui", "--no-ui", "--platform", "--no-platform", "--eslint", "--no-eslint"].some((f) => args.includes(f)) || Boolean(runnersFlag) || // `--python-packages=…` / `--node-packages=…` / `--c-packages=…` are an
|
|
466
|
+
// explicit non-interactive selection: skip the prompt.
|
|
467
|
+
Boolean(packages);
|
|
263
468
|
if (args.includes("--tests"))
|
|
264
469
|
features["tests"] = true;
|
|
265
470
|
if (args.includes("--no-tests"))
|
|
@@ -285,6 +490,15 @@ async function resolveFeatures(templateDir, args) {
|
|
|
285
490
|
for (const key of runnerKeys)
|
|
286
491
|
features[key] = chosen.has(runnerName(key));
|
|
287
492
|
}
|
|
493
|
+
if (packages) {
|
|
494
|
+
if (packages.python?.length && "runner:east-py" in manifest.features)
|
|
495
|
+
features["runner:east-py"] = true;
|
|
496
|
+
if (packages.node?.length && "runner:east-node" in manifest.features)
|
|
497
|
+
features["runner:east-node"] = true;
|
|
498
|
+
if (packages.c?.length && "runner:east-c" in manifest.features)
|
|
499
|
+
features["runner:east-c"] = true;
|
|
500
|
+
features["platform"] = false;
|
|
501
|
+
}
|
|
288
502
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !selectionFlags;
|
|
289
503
|
if (!interactive)
|
|
290
504
|
return features;
|
|
@@ -317,6 +531,28 @@ async function resolveFeatures(templateDir, args) {
|
|
|
317
531
|
}
|
|
318
532
|
return features;
|
|
319
533
|
}
|
|
534
|
+
function parsePackages(args) {
|
|
535
|
+
const grab = (flag) => {
|
|
536
|
+
const arg = args.find((a) => a === flag || a.startsWith(`${flag}=`));
|
|
537
|
+
if (arg === void 0)
|
|
538
|
+
return void 0;
|
|
539
|
+
const value = arg.startsWith(`${flag}=`) ? arg.slice(flag.length + 1) : "";
|
|
540
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
541
|
+
};
|
|
542
|
+
const python = grab("--python-packages");
|
|
543
|
+
const node = grab("--node-packages");
|
|
544
|
+
const c = grab("--c-packages");
|
|
545
|
+
if (!python && !node && !c)
|
|
546
|
+
return void 0;
|
|
547
|
+
const spec = {};
|
|
548
|
+
if (python)
|
|
549
|
+
spec.python = python;
|
|
550
|
+
if (node)
|
|
551
|
+
spec.node = node;
|
|
552
|
+
if (c)
|
|
553
|
+
spec.c = c;
|
|
554
|
+
return spec;
|
|
555
|
+
}
|
|
320
556
|
async function askYesNo(rl, question, def) {
|
|
321
557
|
const answer = (await rl.question(`${question} [${def ? "Y/n" : "y/N"}] `)).trim().toLowerCase();
|
|
322
558
|
if (answer === "")
|
|
@@ -336,6 +572,12 @@ function printHelp(kind) {
|
|
|
336
572
|
console.log(" --ui | --no-ui include east-ui + e3-ui UI components (default: no)");
|
|
337
573
|
console.log(" --platform | --no-platform include a project-owned platform module (TS-East; +Python when east-py is on) (default: no)");
|
|
338
574
|
console.log(" --runners=east-node,east-c,east-py East runtimes to include (default: all)");
|
|
575
|
+
console.log("");
|
|
576
|
+
console.log(" Split platform code into separate workspace packages, each with its own");
|
|
577
|
+
console.log(" execution environment (editing one re-runs only its tasks):");
|
|
578
|
+
console.log(" --python-packages=pricing,common uv workspace members (packages/python/*)");
|
|
579
|
+
console.log(" --node-packages=api npm workspace members (packages/node/*)");
|
|
580
|
+
console.log(" --c-packages=solver native binaries wired via a tools env (packages/native/*)");
|
|
339
581
|
}
|
|
340
582
|
console.log("");
|
|
341
583
|
console.log("Run interactively (a TTY with no feature flags) to be prompted for these.");
|
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Builds the __PACKAGE_NAME__ tool -> build/__PACKAGE_NAME__.
|
|
2
|
+
#
|
|
3
|
+
# e3 captures this binary into the exported bundle via the task's
|
|
4
|
+
# `environment: { tools: { files: [...] } }`, so rebuilding it changes the
|
|
5
|
+
# environment hash and re-runs only the tasks that use it. Build before export:
|
|
6
|
+
# make -C packages/native/__PACKAGE_NAME__
|
|
7
|
+
CC ?= cc
|
|
8
|
+
CFLAGS ?= -O2 -Wall -Wextra
|
|
9
|
+
|
|
10
|
+
build/__PACKAGE_NAME__: src/__PACKAGE_NAME__.c
|
|
11
|
+
mkdir -p build
|
|
12
|
+
$(CC) $(CFLAGS) -o $@ $<
|
|
13
|
+
|
|
14
|
+
.PHONY: clean
|
|
15
|
+
clean:
|
|
16
|
+
rm -rf build
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import e3 from "@elaraai/e3";
|
|
2
|
+
import { East, ArrayType, FloatType } from "@elaraai/east";
|
|
3
|
+
|
|
4
|
+
// A C tool captured via `environment: { tools }`. Unlike python/node packages,
|
|
5
|
+
// e3 does NOT auto-derive a tools environment — you attach the prebuilt binary
|
|
6
|
+
// explicitly, and rebuilding it changes the env hash so only this task re-runs.
|
|
7
|
+
// Build it first: `make -C packages/native/__PACKAGE_NAME__`.
|
|
8
|
+
//
|
|
9
|
+
// This example passes a value through the tool unchanged (the scaffold binary is
|
|
10
|
+
// a passthrough). Replace src/__PACKAGE_NAME__.c with real native logic — embed
|
|
11
|
+
// east-c to decode / compute / encode the BEAST2 dataset files.
|
|
12
|
+
const __PACKAGE_IDENT___values = e3.input("__PACKAGE_IDENT___values", ArrayType(FloatType), [1.0, 2.0, 3.0]);
|
|
13
|
+
|
|
14
|
+
export const __PACKAGE_IDENT___task = e3.customTask(
|
|
15
|
+
"__PACKAGE_IDENT___tool",
|
|
16
|
+
[__PACKAGE_IDENT___values],
|
|
17
|
+
ArrayType(FloatType),
|
|
18
|
+
(_$, inputs, output) => East.str`__PACKAGE_NAME__ ${inputs.get(0n)} ${output}`,
|
|
19
|
+
{
|
|
20
|
+
environment: { tools: { files: ["packages/native/__PACKAGE_NAME__/build/__PACKAGE_NAME__"] } },
|
|
21
|
+
},
|
|
22
|
+
);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The __PACKAGE_NAME__ tool for __DISPLAY_NAME__ — a prebuilt native binary
|
|
3
|
+
* captured into the e3 bundle via `environment: { tools: { files: [...] } }`.
|
|
4
|
+
*
|
|
5
|
+
* A custom-runner tool is invoked as `__PACKAGE_NAME__ <input> <output>`: it
|
|
6
|
+
* reads the input dataset file and writes the output dataset file, both
|
|
7
|
+
* BEAST2-encoded East values. This example is a PASSTHROUGH (it copies input to
|
|
8
|
+
* output — valid when the task's input and output types match) so the scaffold
|
|
9
|
+
* stays dependency-free and builds with a plain C compiler.
|
|
10
|
+
*
|
|
11
|
+
* For real native computation, embed the east-c runtime to decode the input,
|
|
12
|
+
* compute, and encode the output BEAST2 value (see the docs on custom C
|
|
13
|
+
* runners). A rebuild changes the captured binary's hash, so e3 re-runs only
|
|
14
|
+
* the tasks wired to this tool.
|
|
15
|
+
*/
|
|
16
|
+
#include <stdio.h>
|
|
17
|
+
|
|
18
|
+
int main(int argc, char **argv) {
|
|
19
|
+
if (argc < 3) {
|
|
20
|
+
fprintf(stderr, "usage: %s <input> <output>\n", argv[0]);
|
|
21
|
+
return 2;
|
|
22
|
+
}
|
|
23
|
+
FILE *in = fopen(argv[1], "rb");
|
|
24
|
+
if (!in) { perror("open input"); return 1; }
|
|
25
|
+
FILE *out = fopen(argv[2], "wb");
|
|
26
|
+
if (!out) { perror("open output"); fclose(in); return 1; }
|
|
27
|
+
|
|
28
|
+
unsigned char buf[65536];
|
|
29
|
+
size_t n;
|
|
30
|
+
while ((n = fread(buf, 1, sizeof buf, in)) > 0) {
|
|
31
|
+
if (fwrite(buf, 1, n, out) != n) { perror("write output"); fclose(in); fclose(out); return 1; }
|
|
32
|
+
}
|
|
33
|
+
fclose(in);
|
|
34
|
+
fclose(out);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import e3 from "@elaraai/e3";
|
|
2
|
+
import { East, IntegerType, FloatType } from "@elaraai/east";
|
|
3
|
+
|
|
4
|
+
// TS-East mirror of @__PROJECT_NAME__/__PACKAGE_NAME__'s platform function —
|
|
5
|
+
// same dotted "__PACKAGE_NAME__.example" name + signature. Keep this in lockstep
|
|
6
|
+
// with packages/node/__PACKAGE_NAME__/src/platform.ts.
|
|
7
|
+
const example = East.platform("__PACKAGE_NAME__.example", [IntegerType, FloatType], IntegerType);
|
|
8
|
+
|
|
9
|
+
const value = e3.input("__PACKAGE_IDENT___value", IntegerType, 21n);
|
|
10
|
+
const factor = e3.input("__PACKAGE_IDENT___factor", FloatType, 2.0);
|
|
11
|
+
|
|
12
|
+
// No `environment` — e3 derives it from the `{ custom: "@__PROJECT_NAME__/__PACKAGE_NAME__" }`
|
|
13
|
+
// platform reference below, capturing packages/node/__PACKAGE_NAME__'s npm
|
|
14
|
+
// workspace closure into the bundle. Editing that package re-runs only this
|
|
15
|
+
// task; sibling packages stay cached.
|
|
16
|
+
export const __PACKAGE_IDENT___task = e3.task(
|
|
17
|
+
"__PACKAGE_IDENT___example",
|
|
18
|
+
[value, factor],
|
|
19
|
+
East.function([IntegerType, FloatType], IntegerType, ($, v, f) => {
|
|
20
|
+
$.return(example(v, f));
|
|
21
|
+
}),
|
|
22
|
+
{
|
|
23
|
+
runner: { runtime: "east-node", platforms: [{ custom: "@__PROJECT_NAME__/__PACKAGE_NAME__" }] },
|
|
24
|
+
},
|
|
25
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@__PROJECT_NAME__/__PACKAGE_NAME__",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The __PACKAGE_NAME__ platform package for __DISPLAY_NAME__",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": true,
|
|
7
|
+
"exports": {
|
|
8
|
+
"./platform": "./dist/platform.js",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@elaraai/east": "^__VERSION__",
|
|
19
|
+
"@elaraai/east-node-std": "^__VERSION__"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"typescript": "^5.7.2"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { East, IntegerType, FloatType } from "@elaraai/east";
|
|
2
|
+
|
|
3
|
+
// The east-node platform function owned by @__PROJECT_NAME__/__PACKAGE_NAME__.
|
|
4
|
+
// Platform functions let East call native TS/Node code East can't express. Its
|
|
5
|
+
// dotted "__PACKAGE_NAME__.example" name + signature mirror the TS-East
|
|
6
|
+
// declaration the app calls (src/packages/__PACKAGE_NAME__.ts) — keep the two
|
|
7
|
+
// in lockstep. Add native dependencies to this package's package.json and use
|
|
8
|
+
// them in the implementation below.
|
|
9
|
+
const example = East.platform("__PACKAGE_NAME__.example", [IntegerType, FloatType], IntegerType);
|
|
10
|
+
|
|
11
|
+
const exampleImpl = example.implement(
|
|
12
|
+
(value: bigint, factor: number): bigint => BigInt(Math.ceil(Number(value) * factor)),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
// The PlatformFunction[] the east-node runner loads via the package's
|
|
16
|
+
// `@__PROJECT_NAME__/__PACKAGE_NAME__/platform` export.
|
|
17
|
+
export default [exampleImpl];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"exclude": [
|
|
3
|
+
"dist"
|
|
4
|
+
],
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"module": "nodenext",
|
|
9
|
+
"target": "esnext",
|
|
10
|
+
"lib": [
|
|
11
|
+
"esnext"
|
|
12
|
+
],
|
|
13
|
+
"declaration": false,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"strict": true,
|
|
16
|
+
"noUncheckedIndexedAccess": true,
|
|
17
|
+
"exactOptionalPropertyTypes": true,
|
|
18
|
+
"verbatimModuleSyntax": true,
|
|
19
|
+
"isolatedModules": true,
|
|
20
|
+
"moduleDetection": "force",
|
|
21
|
+
"skipLibCheck": true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import e3 from "@elaraai/e3";
|
|
2
|
+
import { East, ArrayType, FloatType } from "@elaraai/east";
|
|
3
|
+
|
|
4
|
+
// TS-East mirror of `packages/python/__PACKAGE_NAME__`'s @platform_function —
|
|
5
|
+
// same dotted "__PACKAGE_NAME__.example" name, same signature. Keep this in
|
|
6
|
+
// lockstep with packages/python/__PACKAGE_NAME__/src/__PACKAGE_NAME__/example.py.
|
|
7
|
+
const example = East.platform("__PACKAGE_NAME__.example", [ArrayType(FloatType)], FloatType);
|
|
8
|
+
|
|
9
|
+
const values = e3.input("__PACKAGE_IDENT___values", ArrayType(FloatType), [1.0, 2.0, 3.0]);
|
|
10
|
+
|
|
11
|
+
// No `environment` needed: e3 derives it from the `{ custom: "__PACKAGE_NAME__" }`
|
|
12
|
+
// platform reference below — at export it captures packages/python/__PACKAGE_NAME__'s
|
|
13
|
+
// dependency closure into the bundle. Editing anything under that package re-runs
|
|
14
|
+
// only this task; sibling packages stay cached. (To override — e.g. pin a
|
|
15
|
+
// container image, or add prebuilt `tools` binaries — add an explicit
|
|
16
|
+
// `environment:` here.)
|
|
17
|
+
export const __PACKAGE_IDENT___task = e3.task(
|
|
18
|
+
"__PACKAGE_IDENT___example",
|
|
19
|
+
[values],
|
|
20
|
+
East.function([ArrayType(FloatType)], FloatType, ($, v) => {
|
|
21
|
+
$.return(example(v));
|
|
22
|
+
}),
|
|
23
|
+
{
|
|
24
|
+
runner: { runtime: "east-py", platforms: [{ custom: "__PACKAGE_NAME__" }, "east-py-std"] },
|
|
25
|
+
},
|
|
26
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "__PACKAGE_NAME__"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "The __PACKAGE_NAME__ platform package for __DISPLAY_NAME__"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"elaraai-east-py",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
# Hatchling gives byte-reproducible sdists, so this package's captured
|
|
11
|
+
# environment hash is stable across exports. (Legacy setuptools embeds file
|
|
12
|
+
# mtimes into the sdist, which would re-invalidate the environment — and re-run
|
|
13
|
+
# every task wired to it — on every export.) Keep hatchling here.
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["hatchling"]
|
|
16
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""The ``__PACKAGE_NAME__`` platform package — its own execution environment.
|
|
2
|
+
|
|
3
|
+
Everything under ``packages/python/__PACKAGE_NAME__`` is captured as ONE
|
|
4
|
+
environment when a task wired to it is exported. Editing this package changes
|
|
5
|
+
only its environment hash, so only the tasks that declare
|
|
6
|
+
``environment: { python: { project: "packages/python/__PACKAGE_NAME__" } }``
|
|
7
|
+
re-run — sibling packages are left untouched. That package boundary IS the
|
|
8
|
+
change-detection granularity.
|
|
9
|
+
|
|
10
|
+
``east-py run -p __PACKAGE_NAME__`` imports this package and reads the top-level
|
|
11
|
+
``platform`` list below. To add a function: create a module beside this file
|
|
12
|
+
(e.g. ``pricing.py``) ending with ``pricing_impl = platform_functions(__name__)``,
|
|
13
|
+
then import and spread it into ``platform``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .example import example_impl
|
|
17
|
+
|
|
18
|
+
platform = [*example_impl]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""An example Python platform function owned by the ``__PACKAGE_NAME__`` package.
|
|
2
|
+
|
|
3
|
+
Platform functions let East call NATIVE Python (numpy, pandas, scikit-learn, …)
|
|
4
|
+
that East itself can't express. The ``@platform_function`` is bound to East by
|
|
5
|
+
its dotted ``"__PACKAGE_NAME__.<fn>"`` name, which the TypeScript declaration in
|
|
6
|
+
``src/packages/__PACKAGE_NAME__.ts`` mirrors exactly — keep the two in lockstep.
|
|
7
|
+
Add native dependencies to this package's ``pyproject.toml`` and import them
|
|
8
|
+
inside the body.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from east.runtime.platform import platform_function, platform_functions
|
|
12
|
+
from east.types.types import ArrayType, FloatType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@platform_function(
|
|
16
|
+
inputs=[ArrayType(FloatType)],
|
|
17
|
+
output=FloatType,
|
|
18
|
+
name="__PACKAGE_NAME__.example",
|
|
19
|
+
)
|
|
20
|
+
def example(values):
|
|
21
|
+
"""Example: the mean of a list of floats. Replace with your own logic."""
|
|
22
|
+
values = list(values)
|
|
23
|
+
return sum(values) / len(values) if values else 0.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# The platform functions defined in THIS module, in definition order.
|
|
27
|
+
example_impl = platform_functions(__name__)
|