dotframe 0.1.4 → 0.1.5
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/CHANGELOG.md +4 -0
- package/cli/native.ts +96 -37
- package/package.json +1 -1
- package/skill-data/macos/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.5
|
|
4
|
+
|
|
5
|
+
- Native builds follow the entry's import graph instead of copying the game folder: relative imports that leave the game root and workspace packages that resolve to source are staged with their relative layout and rewritten to relative paths, so ports inside monorepos compile.
|
|
6
|
+
|
|
3
7
|
## 0.1.4
|
|
4
8
|
|
|
5
9
|
Fixes from rounds 2 and 3 of the Craft Ones dogfood.
|
package/cli/native.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { CliError, type Ctx, exec, home, print, type RunResult, which } from "./lib";
|
|
5
5
|
|
|
6
6
|
// The engine this CLI belongs to: a git checkout or node_modules/dotframe.
|
|
@@ -44,42 +44,104 @@ export function vendorFix(platform: NativePlatform): string {
|
|
|
44
44
|
return `dotframe vendor ${platform}`;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
const SPECIFIER = /(?:from\s+|import\s*\(\s*|import\s+)(["'])([^"']+)\1/g;
|
|
48
|
+
const EXTENSIONS = ["", ".ts", ".tsx", ".json", "/index.ts", "/index.tsx"];
|
|
49
|
+
|
|
50
|
+
function resolveFile(base: string): string | null {
|
|
51
|
+
const candidates = [base.replace(/\.js$/, ".ts"), base];
|
|
52
|
+
for (const c of candidates) for (const ext of EXTENSIONS) if (existsSync(c + ext) && statSync(c + ext).isFile()) return realpathSync(c + ext);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A bare specifier that resolves, through node_modules, to TypeScript source outside node_modules: a workspace
|
|
57
|
+
// package (bun and npm link those). Published packages stay packages.
|
|
58
|
+
function resolveWorkspace(specifier: string, fromDir: string): string | null {
|
|
59
|
+
const parts = specifier.split("/");
|
|
60
|
+
const name = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
61
|
+
const sub = parts.slice(name.split("/").length).join("/");
|
|
62
|
+
for (let dir = fromDir; ; dir = dirname(dir)) {
|
|
63
|
+
const pkgDir = join(dir, "node_modules", name);
|
|
64
|
+
if (existsSync(pkgDir)) {
|
|
65
|
+
const real = realpathSync(pkgDir);
|
|
66
|
+
if (real.split(sep).includes("node_modules")) return null;
|
|
67
|
+
if (sub) return resolveFile(join(real, sub));
|
|
68
|
+
const pkg = JSON.parse(readFileSync(join(real, "package.json"), "utf8")) as Record<string, unknown>;
|
|
69
|
+
const exp = pkg.exports as Record<string, unknown> | string | undefined;
|
|
70
|
+
const dot = typeof exp === "string" ? exp : (exp?.["."] as Record<string, string> | string | undefined);
|
|
71
|
+
const entry = typeof dot === "string" ? dot : (dot?.import ?? dot?.default ?? dot?.types ?? (pkg.module as string) ?? (pkg.main as string) ?? "index.ts");
|
|
72
|
+
return resolveFile(join(real, entry));
|
|
59
73
|
}
|
|
74
|
+
if (dirname(dir) === dir) return null;
|
|
60
75
|
}
|
|
61
|
-
return n;
|
|
62
76
|
}
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
interface Graph {
|
|
79
|
+
files: string[];
|
|
80
|
+
// Per file, the specifiers to rewrite and the absolute file each one points at.
|
|
81
|
+
links: Map<string, Map<string, string>>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Every source file the entry reaches: relative imports (including ones that leave the game root) and workspace
|
|
85
|
+
// packages. "dotframe/..." is the engine, staged separately.
|
|
86
|
+
function importGraph(entry: string): Graph {
|
|
87
|
+
const files: string[] = [];
|
|
88
|
+
const links = new Map<string, Map<string, string>>();
|
|
89
|
+
const queue = [realpathSync(entry)];
|
|
90
|
+
const seen = new Set(queue);
|
|
91
|
+
while (queue.length > 0) {
|
|
92
|
+
const file = queue.shift() as string;
|
|
93
|
+
files.push(file);
|
|
94
|
+
if (!/\.tsx?$/.test(file)) continue;
|
|
95
|
+
const map = new Map<string, string>();
|
|
96
|
+
for (const m of readFileSync(file, "utf8").matchAll(SPECIFIER)) {
|
|
97
|
+
const spec = m[2];
|
|
98
|
+
if (spec.startsWith("dotframe/") || spec.startsWith("node:")) continue;
|
|
99
|
+
const target = spec.startsWith(".") ? resolveFile(resolve(dirname(file), spec)) : resolveWorkspace(spec, dirname(file));
|
|
100
|
+
if (!target) continue;
|
|
101
|
+
if (!spec.startsWith(".")) map.set(spec, target);
|
|
102
|
+
if (!seen.has(target)) {
|
|
103
|
+
seen.add(target);
|
|
104
|
+
queue.push(target);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
links.set(file, map);
|
|
108
|
+
}
|
|
109
|
+
return { files, links };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function commonDir(paths: string[]): string {
|
|
113
|
+
let base = dirname(paths[0]);
|
|
114
|
+
for (const p of paths) while (!(p + sep).startsWith(base === sep ? sep : base + sep)) base = dirname(base);
|
|
115
|
+
return base;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function rel(fromFile: string, to: string): string {
|
|
119
|
+
const r = relative(dirname(fromFile), to);
|
|
120
|
+
return r.startsWith(".") ? r : `./${r}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// scriptc's static build takes relative imports only, and treats anything under node_modules as package code for
|
|
124
|
+
// its dynamic engine. Staging copies the engine and every reached game file side by side, keeping their relative
|
|
125
|
+
// layout, and rewrites "dotframe/..." and workspace imports to relative paths.
|
|
126
|
+
function stageGame(root: string, entry: string, tree: string, engineStage: string): string {
|
|
127
|
+
const graph = importGraph(join(root, entry));
|
|
128
|
+
const base = commonDir([realpathSync(root) + sep + "x", ...graph.files]);
|
|
129
|
+
const staged = (file: string): string => join(tree, relative(base, file));
|
|
130
|
+
for (const file of graph.files) {
|
|
131
|
+
const to = staged(file);
|
|
132
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
133
|
+
let text = readFileSync(file, "utf8");
|
|
134
|
+
if (/\.tsx?$/.test(file)) {
|
|
135
|
+
const map = graph.links.get(file) ?? new Map<string, string>();
|
|
136
|
+
text = text.replace(SPECIFIER, (whole: string, q: string, spec: string): string => {
|
|
137
|
+
if (spec.startsWith("dotframe/")) return whole.replace(`${q}${spec}${q}`, `${q}${rel(to, join(engineStage, spec.slice("dotframe/".length)))}${q}`);
|
|
138
|
+
const target = map.get(spec);
|
|
139
|
+
return target ? whole.replace(`${q}${spec}${q}`, `${q}${rel(to, staged(target)).replace(/\.tsx?$/, "")}${q}`) : whole;
|
|
140
|
+
});
|
|
73
141
|
}
|
|
74
|
-
|
|
75
|
-
const text = readFileSync(path, "utf8");
|
|
76
|
-
const next = text.replace(/(from\s+|import\s*\(\s*)(["'])dotframe\/([^"']+)\2/g, (_m: string, head: string, q: string, rest: string): string => {
|
|
77
|
-
let rel = relative(dirname(path), join(engineRoot, rest));
|
|
78
|
-
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
79
|
-
return `${head}${q}${rel}${q}`;
|
|
80
|
-
});
|
|
81
|
-
if (next !== text) writeFileSync(path, next);
|
|
142
|
+
writeFileSync(to, text);
|
|
82
143
|
}
|
|
144
|
+
return join(tree, relative(base, realpathSync(root)));
|
|
83
145
|
}
|
|
84
146
|
|
|
85
147
|
export async function buildNative(ctx: Ctx, root: string, gameName: string, targetName: string, t: NativeTarget): Promise<{ binary: string; log: string; steps: RunResult[] }> {
|
|
@@ -95,6 +157,7 @@ export async function buildNative(ctx: Ctx, root: string, gameName: string, targ
|
|
|
95
157
|
const log = join(logDir, `build-${targetName}.log`);
|
|
96
158
|
const out = join(root, "dist", targetName);
|
|
97
159
|
const binary = join(out, `${t.name ?? gameName}${platform === "windows" ? ".exe" : ""}`);
|
|
160
|
+
if (!existsSync(join(root, t.entry))) throw new CliError("ENTRY_MISSING", `native entry ${t.entry} does not exist`, `add ${t.entry} (see the macos skill) or fix targets.${targetName}.native.entry`, skill);
|
|
98
161
|
if (ctx.dryRun) {
|
|
99
162
|
print(ctx, { dryRun: true, platform, stage, vendor: vendor.dir, binary }, (): string => `would stage the engine and game in ${stage} and build ${binary}`);
|
|
100
163
|
return { binary, log, steps: [] };
|
|
@@ -104,9 +167,7 @@ export async function buildNative(ctx: Ctx, root: string, gameName: string, targ
|
|
|
104
167
|
mkdirSync(out, { recursive: true });
|
|
105
168
|
const engineStage = join(stage, "dotframe");
|
|
106
169
|
cpSync(join(ENGINE, "src"), join(engineStage, "src"), { recursive: true });
|
|
107
|
-
const gameStage = join(stage, "game");
|
|
108
|
-
copyTs(root, gameStage);
|
|
109
|
-
rewriteImports(gameStage, engineStage);
|
|
170
|
+
const gameStage = stageGame(root, t.entry, join(stage, "game"), engineStage);
|
|
110
171
|
|
|
111
172
|
const inc = [`-I${join(vendor.dir, vendor.sdl ?? "", "include")}`, `-I${join(vendor.dir, "wgpu", platform, "include")}`];
|
|
112
173
|
const lib = join(stage, "lib");
|
|
@@ -120,8 +181,6 @@ export async function buildNative(ctx: Ctx, root: string, gameName: string, targ
|
|
|
120
181
|
const ffi = JSON.parse(readFileSync(join(ENGINE, "native", `ffi.${platform}.json`), "utf8")) as { libraries: string[] };
|
|
121
182
|
ffi.libraries = [join(lib, "libdf_native.a"), join(vendor.dir, "build", `sdl-${platform}`, "libSDL3.a"), join(vendor.dir, "wgpu", platform, "lib", "libwgpu_native.a")];
|
|
122
183
|
writeFileSync(join(stage, "ffi.json"), JSON.stringify(ffi, null, 2));
|
|
123
|
-
const entry = join(gameStage, t.entry);
|
|
124
|
-
if (!existsSync(entry)) throw new CliError("ENTRY_MISSING", `native entry ${t.entry} does not exist`, `add ${t.entry} (see the macos skill) or fix targets.${targetName}.native.entry`, skill);
|
|
125
184
|
let env: Record<string, string> = {};
|
|
126
185
|
if (platform === "windows") {
|
|
127
186
|
// A devDependency of dotframe, so an npm install of dotframe does not bring it; the game can.
|
package/package.json
CHANGED
|
@@ -20,7 +20,7 @@ A target with `"native"` is built by the CLI itself:
|
|
|
20
20
|
"windows": { "native": { "platform": "windows", "entry": "main.native.ts" } }
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
The CLI copies the engine (`src/`) and
|
|
23
|
+
The CLI copies the engine (`src/`) and every file the entry reaches into `.dotframe/native/<target>/`: relative imports (including ones that leave the game folder, as in a monorepo port importing `../../packages/shared/src`) and workspace packages that resolve to source (`@my/shared` linked by bun or npm workspaces). It keeps their relative layout, rewrites `dotframe/...` and workspace imports to relative paths, compiles the C shim against the vendored SDL3 and wgpu-native, and runs scriptc. This is why it works from `node_modules`: scriptc treats code under `node_modules` as package code for its dynamic engine and takes only relative imports in a static build, so building in place fails.
|
|
24
24
|
|
|
25
25
|
- Vendor lives in `DOTFRAME_VENDOR`, else a dotframe git checkout's `vendor/` when populated, else `~/.dotframe/vendor`. It survives reinstalling dotframe and is shared by every game.
|
|
26
26
|
- Output goes to the game's `dist/<target>/`, never into `node_modules`.
|