cluaupp 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.2
6
+
7
+ - Header modules with a sibling `.cpp` now re-export those functions (`startingCoins` from `config.cpp` lands on `config.luau` via `configImpl`). The impl no longer `require`s its own header (`ReplicatedStorage.config`).
8
+
9
+ ## 0.2.1
10
+
11
+ - Pin `tree-sitter` 0.21.1 and `tree-sitter-c` 0.23.2 so `npm i -g cluaupp` no longer pulls `tree-sitter-c@0.23.6` (peerOptional `tree-sitter@^0.22.1`).
12
+
5
13
  ## 0.2.0
6
14
 
7
15
  - CLI rewritten as a TypeScript collector-emitter pipeline: Tree-sitter collection, Rojo-mapped `require`s, ordered Luau blocks (Services → Requires → Types → Constants → Code), then optional StyLua / `luau-analyze`. Commands `init`, `build`, `watch`, `lsp`, and `intellisense` stay. `cluaupp build -i file.cpp -o out.luau --rojo default.project.json` is the single-file path.
@@ -1,25 +1,25 @@
1
- {
2
- "name": "cluaupp-diagnostics",
3
- "displayName": "Cluaupp",
4
- "description": "Cluaupp subset parse errors. C++ completion, hover, and go-to-definition come from clangd + include/cluaupp/roblox.hpp.",
5
- "version": "0.2.0",
6
- "publisher": "kartzdev",
7
- "engines": {
8
- "vscode": "^1.74.0"
9
- },
10
- "categories": [
11
- "Linters"
12
- ],
13
- "activationEvents": [
14
- "onLanguage:cpp"
15
- ],
16
- "main": "./extension.js",
17
- "contributes": {
18
- "commands": [
19
- {
20
- "command": "cluaupp.restartIntelliSense",
21
- "title": "Cluaupp: Reload subset diagnostics"
22
- }
23
- ]
24
- }
25
- }
1
+ {
2
+ "name": "cluaupp-diagnostics",
3
+ "displayName": "Cluaupp",
4
+ "description": "Cluaupp subset parse errors. C++ completion, hover, and go-to-definition come from clangd + include/cluaupp/roblox.hpp.",
5
+ "version": "0.2.2",
6
+ "publisher": "kartzdev",
7
+ "engines": {
8
+ "vscode": "^1.74.0"
9
+ },
10
+ "categories": [
11
+ "Linters"
12
+ ],
13
+ "activationEvents": [
14
+ "onLanguage:cpp"
15
+ ],
16
+ "main": "./extension.js",
17
+ "contributes": {
18
+ "commands": [
19
+ {
20
+ "command": "cluaupp.restartIntelliSense",
21
+ "title": "Cluaupp: Reload subset diagnostics"
22
+ }
23
+ ]
24
+ }
25
+ }
@@ -1,3 +1,5 @@
1
1
  #include <cluaupp/roblox.hpp>
2
2
 
3
3
  const int STARTING_COINS = 0;
4
+
5
+ int startingCoins();
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.compileSource = compileSource;
7
7
  exports.compileService = compileService;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
10
  const parse_js_1 = require("./parse.js");
10
11
  const emit_js_1 = require("./emit.js");
@@ -33,12 +34,49 @@ function attachRequires(contents, source, ast, options, outName) {
33
34
  function posixRel(from, file) {
34
35
  return node_path_1.default.relative(from, file).replace(/\\/g, "/");
35
36
  }
37
+ function declKey(decl) {
38
+ return `${decl.owner || ""}::${decl.name}`;
39
+ }
40
+ function mergeSiblingImpl(ast, implPath, options) {
41
+ const implOptions = {
42
+ ...options,
43
+ moduleIncludes: [],
44
+ seen: new Set(),
45
+ pragmaResolved: false,
46
+ };
47
+ const prepared = (0, preprocess_js_1.preprocess)(node_fs_1.default.readFileSync(implPath, "utf8"), implPath, implOptions);
48
+ const implAst = (0, parse_js_1.parse)(prepared, node_path_1.default.basename(implPath));
49
+ if (!Array.isArray(ast.body)) {
50
+ ast.body = [];
51
+ }
52
+ const existing = new Set((ast.body || [])
53
+ .filter((decl) => decl && (decl.type === "function" || decl.type === "proto") && decl.name)
54
+ .map(declKey));
55
+ for (const decl of implAst.body || []) {
56
+ if ((decl.type !== "function" && decl.type !== "proto") || !decl.name) {
57
+ continue;
58
+ }
59
+ const key = declKey(decl);
60
+ if (existing.has(key)) {
61
+ continue;
62
+ }
63
+ existing.add(key);
64
+ ast.body.push({
65
+ type: "proto",
66
+ name: decl.name,
67
+ owner: decl.owner,
68
+ returnType: decl.returnType,
69
+ params: decl.params,
70
+ });
71
+ }
72
+ }
36
73
  function wireHeaderImpl(ast, fileName, options) {
37
74
  const headerPath = options.filePath || null;
38
75
  const impl = headerPath ? (0, preprocess_js_1.siblingImplementation)(headerPath) : null;
39
76
  if (!impl) {
40
77
  return;
41
78
  }
79
+ mergeSiblingImpl(ast, impl, options);
42
80
  const typeName = (0, headers_js_1.primaryHeaderName)(ast, options.relativeName || fileName);
43
81
  const implRel = options.srcDir ? posixRel(options.srcDir, impl) : node_path_1.default.basename(impl).replace(/\\/g, "/");
44
82
  const implOut = (0, preprocess_js_1.implOutName)(implRel);
@@ -1,7 +1,12 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.ASTCollector = void 0;
4
7
  exports.emptyState = emptyState;
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const preprocess_js_1 = require("../preprocess.js");
5
10
  const translators_js_1 = require("../emitter/translators.js");
6
11
  function emptyState(strict = false) {
7
12
  return {
@@ -16,9 +21,11 @@ function emptyState(strict = false) {
16
21
  }
17
22
  class ASTCollector {
18
23
  mapper;
24
+ context;
19
25
  state;
20
26
  constructor(mapper, context = { fileName: "input.cpp" }) {
21
27
  this.mapper = mapper;
28
+ this.context = context;
22
29
  this.state = emptyState(context.strict === true);
23
30
  }
24
31
  collect(rootNode) {
@@ -58,12 +65,41 @@ class ASTCollector {
58
65
  }
59
66
  }
60
67
  }
68
+ includeTarget(raw) {
69
+ const from = this.context.sourcePath;
70
+ if (!from) {
71
+ return null;
72
+ }
73
+ const cleaned = raw.replace(/[<>'"]/g, "").trim();
74
+ if (!cleaned || raw.includes("<")) {
75
+ return null;
76
+ }
77
+ return node_path_1.default.resolve(node_path_1.default.dirname(from), cleaned);
78
+ }
79
+ isOwnHeader(raw) {
80
+ const from = this.context.sourcePath;
81
+ if (!from) {
82
+ return false;
83
+ }
84
+ const target = this.includeTarget(raw);
85
+ if (!target) {
86
+ return false;
87
+ }
88
+ if (node_path_1.default.resolve(from) === target) {
89
+ return true;
90
+ }
91
+ const header = (0, preprocess_js_1.siblingHeader)(from);
92
+ return Boolean(header && node_path_1.default.resolve(header) === target);
93
+ }
61
94
  collectInclude(node) {
62
95
  const pathNode = node.childForFieldName("path") || node.namedChildren[0] || node.child(1);
63
96
  if (!pathNode) {
64
97
  return;
65
98
  }
66
- const resolved = this.mapper.resolveIncludeToRequire(pathNode.text);
99
+ if (this.isOwnHeader(pathNode.text)) {
100
+ return;
101
+ }
102
+ const resolved = this.mapper.resolveIncludeToRequire(pathNode.text, this.context.sourcePath);
67
103
  if (resolved) {
68
104
  this.state.moduleRequires.set(resolved.alias, resolved.path);
69
105
  }
@@ -64,7 +64,23 @@ class RojoMapper {
64
64
  this.config = (0, comment_json_1.parse)(content);
65
65
  this.bindings = this.walkTree(this.config.tree, []);
66
66
  }
67
- resolveIncludeToRequire(includePath) {
67
+ resolveQuotedFile(raw, fromFile) {
68
+ const bases = [];
69
+ if (fromFile) {
70
+ bases.push(node_path_1.default.dirname(fromFile));
71
+ }
72
+ if (this.srcDir) {
73
+ bases.push(this.srcDir);
74
+ }
75
+ for (const base of bases) {
76
+ const candidate = node_path_1.default.resolve(base, raw);
77
+ if (node_fs_1.default.existsSync(candidate) && node_fs_1.default.statSync(candidate).isFile()) {
78
+ return candidate;
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+ resolveIncludeToRequire(includePath, fromFile) {
68
84
  const raw = includePath.replace(/[<>'"]/g, "").trim();
69
85
  if (!raw) {
70
86
  return null;
@@ -79,11 +95,15 @@ class RojoMapper {
79
95
  if (library) {
80
96
  return library;
81
97
  }
82
- const fromTree = this.resolveFromRojoTree(cleanPath, baseName);
98
+ const resolvedFile = this.resolveQuotedFile(raw, fromFile);
99
+ const lookupPath = resolvedFile && this.srcDir
100
+ ? posix(node_path_1.default.relative(this.srcDir, resolvedFile)).replace(/\.h(pp|h)?$/i, "")
101
+ : cleanPath;
102
+ const fromTree = this.resolveFromRojoTree(lookupPath, baseName);
83
103
  if (fromTree) {
84
104
  return fromTree;
85
105
  }
86
- const segments = cleanPath.split("/").filter(Boolean);
106
+ const segments = lookupPath.split("/").filter(Boolean);
87
107
  if (segments[0] === "src") {
88
108
  segments.shift();
89
109
  }
package/package.json CHANGED
@@ -1,83 +1,87 @@
1
- {
2
- "name": "cluaupp",
3
- "version": "0.2.0",
4
- "description": "Cluaupp — the definitive merge of C++ and modern Luau. Source-to-source transpiler with first-class Roblox APIs.",
5
- "author": "KartzDev",
6
- "license": "MIT",
7
- "bin": {
8
- "cluaupp": "bin/cluaupp.js",
9
- "cluau": "bin/cluaupp.js"
10
- },
11
- "main": "./generated/compile.js",
12
- "exports": {
13
- ".": "./generated/compile.js",
14
- "./package.json": "./package.json"
15
- },
16
- "files": [
17
- "bin",
18
- "src",
19
- "generated",
20
- "include",
21
- "templates",
22
- "examples",
23
- "editors",
24
- "runtime",
25
- "README.md",
26
- "LICENSE",
27
- "CHANGELOG.md",
28
- "docs"
29
- ],
30
- "scripts": {
31
- "cluaupp": "node bin/cluaupp.js",
32
- "cluau": "node bin/cluaupp.js",
33
- "build": "tsc",
34
- "build:cli": "tsc",
35
- "dev:cli": "tsx src/cli.ts",
36
- "prepare": "tsc",
37
- "test": "node test/out.test.js && node test/types.test.js && node test/modules.test.js && node test/security.test.js && node test/understander.test.js",
38
- "generate-api": "node scripts/generate-api.js",
39
- "check-highlight": "node scripts/check-highlight.js",
40
- "vendor-libs": "node scripts/vendor-libs.js",
41
- "docs": "moonwave dev",
42
- "docs:build": "moonwave build",
43
- "site": "node scripts/generate-api.js && node scripts/check-highlight.js",
44
- "dev": "node scripts/dev.js",
45
- "stop": "node scripts/stop.js",
46
- "prepublishOnly": "tsc && npm test"
47
- },
48
- "engines": {
49
- "node": ">=18"
50
- },
51
- "keywords": [
52
- "luau",
53
- "roblox",
54
- "cpp",
55
- "c++",
56
- "transpiler",
57
- "compiler",
58
- "rojo",
59
- "cluaupp",
60
- "cluau"
61
- ],
62
- "repository": {
63
- "type": "git",
64
- "url": "git+https://github.com/KartzRbx/Cluaupp.git"
65
- },
66
- "bugs": {
67
- "url": "https://github.com/KartzRbx/Cluaupp/issues"
68
- },
69
- "homepage": "https://github.com/KartzRbx/Cluaupp#readme",
70
- "dependencies": {
71
- "commander": "^15.0.0",
72
- "comment-json": "^5.0.0",
73
- "tree-sitter": "^0.21.1",
74
- "tree-sitter-cpp": "^0.23.4",
75
- "vscode-languageserver": "^10.1.1",
76
- "vscode-languageserver-textdocument": "^1.0.14"
77
- },
78
- "devDependencies": {
79
- "@types/node": "^22.20.3",
80
- "tsx": "^4.23.13",
81
- "typescript": "^7.0.2"
82
- }
83
- }
1
+ {
2
+ "name": "cluaupp",
3
+ "version": "0.2.2",
4
+ "description": "Cluaupp — the definitive merge of C++ and modern Luau. Source-to-source transpiler with first-class Roblox APIs.",
5
+ "author": "KartzDev",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "cluaupp": "bin/cluaupp.js",
9
+ "cluau": "bin/cluaupp.js"
10
+ },
11
+ "main": "./generated/compile.js",
12
+ "exports": {
13
+ ".": "./generated/compile.js",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "bin",
18
+ "src",
19
+ "generated",
20
+ "include",
21
+ "templates",
22
+ "examples",
23
+ "editors",
24
+ "runtime",
25
+ "README.md",
26
+ "LICENSE",
27
+ "CHANGELOG.md",
28
+ "docs"
29
+ ],
30
+ "scripts": {
31
+ "cluaupp": "node bin/cluaupp.js",
32
+ "cluau": "node bin/cluaupp.js",
33
+ "build": "tsc",
34
+ "build:cli": "tsc",
35
+ "dev:cli": "tsx src/cli.ts",
36
+ "prepare": "tsc",
37
+ "test": "node test/out.test.js && node test/types.test.js && node test/modules.test.js && node test/security.test.js && node test/understander.test.js",
38
+ "generate-api": "node scripts/generate-api.js",
39
+ "check-highlight": "node scripts/check-highlight.js",
40
+ "vendor-libs": "node scripts/vendor-libs.js",
41
+ "docs": "moonwave dev",
42
+ "docs:build": "moonwave build",
43
+ "site": "node scripts/generate-api.js && node scripts/check-highlight.js",
44
+ "dev": "node scripts/dev.js",
45
+ "stop": "node scripts/stop.js",
46
+ "prepublishOnly": "tsc && npm test"
47
+ },
48
+ "engines": {
49
+ "node": ">=18"
50
+ },
51
+ "keywords": [
52
+ "luau",
53
+ "roblox",
54
+ "cpp",
55
+ "c++",
56
+ "transpiler",
57
+ "compiler",
58
+ "rojo",
59
+ "cluaupp",
60
+ "cluau"
61
+ ],
62
+ "repository": {
63
+ "type": "git",
64
+ "url": "git+https://github.com/KartzRbx/Cluaupp.git"
65
+ },
66
+ "bugs": {
67
+ "url": "https://github.com/KartzRbx/Cluaupp/issues"
68
+ },
69
+ "homepage": "https://github.com/KartzRbx/Cluaupp#readme",
70
+ "dependencies": {
71
+ "commander": "^15.0.0",
72
+ "comment-json": "^5.0.0",
73
+ "tree-sitter": "0.21.1",
74
+ "tree-sitter-c": "0.23.2",
75
+ "tree-sitter-cpp": "0.23.4",
76
+ "vscode-languageserver": "^10.1.1",
77
+ "vscode-languageserver-textdocument": "^1.0.14"
78
+ },
79
+ "overrides": {
80
+ "tree-sitter-c": "0.23.2"
81
+ },
82
+ "devDependencies": {
83
+ "@types/node": "^22.20.3",
84
+ "tsx": "^4.23.13",
85
+ "typescript": "^7.0.2"
86
+ }
87
+ }
package/src/compile.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
  import { parse } from "./parse.js";
3
4
  import { emit } from "./emit.js";
@@ -5,7 +6,7 @@ import { preprocess, isHeaderFile, toLuauPath, siblingImplementation, siblingHea
5
6
  import { collectLibraries, insertPreamble } from "./libs.js";
6
7
  import { planOutput } from "./architecture.js";
7
8
  import { emitHeaderModule, primaryHeaderName } from "./headers.js";
8
- import type { AstProgram, ModuleInclude, PreprocessOptions } from "./ast.js";
9
+ import type { AstNode, AstProgram, ModuleInclude, PreprocessOptions } from "./ast.js";
9
10
  import type { CompileOptions, CompileServiceResult } from "./types.js";
10
11
 
11
12
  function shouldAttachRequires(name: string, contents: string): boolean {
@@ -32,12 +33,53 @@ function posixRel(from: string, file: string): string {
32
33
  return path.relative(from, file).replace(/\\/g, "/");
33
34
  }
34
35
 
36
+ function declKey(decl: AstNode): string {
37
+ return `${decl.owner || ""}::${decl.name}`;
38
+ }
39
+
40
+ function mergeSiblingImpl(ast: AstProgram, implPath: string, options: PreprocessOptions): void {
41
+ const implOptions: PreprocessOptions = {
42
+ ...options,
43
+ moduleIncludes: [],
44
+ seen: new Set(),
45
+ pragmaResolved: false,
46
+ };
47
+ const prepared = preprocess(fs.readFileSync(implPath, "utf8"), implPath, implOptions);
48
+ const implAst = parse(prepared, path.basename(implPath)) as AstProgram;
49
+ if (!Array.isArray(ast.body)) {
50
+ ast.body = [];
51
+ }
52
+ const existing = new Set(
53
+ (ast.body || [])
54
+ .filter((decl) => decl && (decl.type === "function" || decl.type === "proto") && decl.name)
55
+ .map(declKey),
56
+ );
57
+ for (const decl of implAst.body || []) {
58
+ if ((decl.type !== "function" && decl.type !== "proto") || !decl.name) {
59
+ continue;
60
+ }
61
+ const key = declKey(decl);
62
+ if (existing.has(key)) {
63
+ continue;
64
+ }
65
+ existing.add(key);
66
+ ast.body.push({
67
+ type: "proto",
68
+ name: decl.name,
69
+ owner: decl.owner,
70
+ returnType: decl.returnType,
71
+ params: decl.params,
72
+ });
73
+ }
74
+ }
75
+
35
76
  function wireHeaderImpl(ast: AstProgram, fileName: string, options: PreprocessOptions): void {
36
77
  const headerPath = options.filePath || null;
37
78
  const impl = headerPath ? siblingImplementation(headerPath) : null;
38
79
  if (!impl) {
39
80
  return;
40
81
  }
82
+ mergeSiblingImpl(ast, impl, options);
41
83
  const typeName = primaryHeaderName(ast, options.relativeName || fileName);
42
84
  const implRel = options.srcDir ? posixRel(options.srcDir, impl) : path.basename(impl).replace(/\\/g, "/");
43
85
  const implOut = implOutName(implRel);
@@ -1,6 +1,8 @@
1
+ import path from "node:path";
1
2
  import type Parser from "tree-sitter";
2
3
  import type { CollectorContext, TranspilerState } from "../types.js";
3
4
  import type { RojoMapper } from "../utils/rojo-mapper.js";
5
+ import { siblingHeader } from "../preprocess.js";
4
6
  import {
5
7
  translateAlias,
6
8
  translateConstant,
@@ -28,7 +30,7 @@ export class ASTCollector {
28
30
 
29
31
  constructor(
30
32
  private mapper: RojoMapper,
31
- context: CollectorContext = { fileName: "input.cpp" },
33
+ private context: CollectorContext = { fileName: "input.cpp" },
32
34
  ) {
33
35
  this.state = emptyState(context.strict === true);
34
36
  }
@@ -68,12 +70,43 @@ export class ASTCollector {
68
70
  }
69
71
  }
70
72
 
73
+ private includeTarget(raw: string): string | null {
74
+ const from = this.context.sourcePath;
75
+ if (!from) {
76
+ return null;
77
+ }
78
+ const cleaned = raw.replace(/[<>'"]/g, "").trim();
79
+ if (!cleaned || raw.includes("<")) {
80
+ return null;
81
+ }
82
+ return path.resolve(path.dirname(from), cleaned);
83
+ }
84
+
85
+ private isOwnHeader(raw: string): boolean {
86
+ const from = this.context.sourcePath;
87
+ if (!from) {
88
+ return false;
89
+ }
90
+ const target = this.includeTarget(raw);
91
+ if (!target) {
92
+ return false;
93
+ }
94
+ if (path.resolve(from) === target) {
95
+ return true;
96
+ }
97
+ const header = siblingHeader(from);
98
+ return Boolean(header && path.resolve(header) === target);
99
+ }
100
+
71
101
  private collectInclude(node: SyntaxNode): void {
72
102
  const pathNode = node.childForFieldName("path") || node.namedChildren[0] || node.child(1);
73
103
  if (!pathNode) {
74
104
  return;
75
105
  }
76
- const resolved = this.mapper.resolveIncludeToRequire(pathNode.text);
106
+ if (this.isOwnHeader(pathNode.text)) {
107
+ return;
108
+ }
109
+ const resolved = this.mapper.resolveIncludeToRequire(pathNode.text, this.context.sourcePath);
77
110
  if (resolved) {
78
111
  this.state.moduleRequires.set(resolved.alias, resolved.path);
79
112
  }
@@ -64,7 +64,24 @@ export class RojoMapper {
64
64
  this.bindings = this.walkTree(this.config.tree, []);
65
65
  }
66
66
 
67
- public resolveIncludeToRequire(includePath: string): RequireBinding | null {
67
+ private resolveQuotedFile(raw: string, fromFile?: string): string | null {
68
+ const bases: string[] = [];
69
+ if (fromFile) {
70
+ bases.push(path.dirname(fromFile));
71
+ }
72
+ if (this.srcDir) {
73
+ bases.push(this.srcDir);
74
+ }
75
+ for (const base of bases) {
76
+ const candidate = path.resolve(base, raw);
77
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
78
+ return candidate;
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+
84
+ public resolveIncludeToRequire(includePath: string, fromFile?: string): RequireBinding | null {
68
85
  const raw = includePath.replace(/[<>'"]/g, "").trim();
69
86
  if (!raw) {
70
87
  return null;
@@ -82,12 +99,18 @@ export class RojoMapper {
82
99
  return library;
83
100
  }
84
101
 
85
- const fromTree = this.resolveFromRojoTree(cleanPath, baseName);
102
+ const resolvedFile = this.resolveQuotedFile(raw, fromFile);
103
+ const lookupPath =
104
+ resolvedFile && this.srcDir
105
+ ? posix(path.relative(this.srcDir, resolvedFile)).replace(/\.h(pp|h)?$/i, "")
106
+ : cleanPath;
107
+
108
+ const fromTree = this.resolveFromRojoTree(lookupPath, baseName);
86
109
  if (fromTree) {
87
110
  return fromTree;
88
111
  }
89
112
 
90
- const segments = cleanPath.split("/").filter(Boolean);
113
+ const segments = lookupPath.split("/").filter(Boolean);
91
114
  if (segments[0] === "src") {
92
115
  segments.shift();
93
116
  }
@@ -1,3 +1,5 @@
1
1
  #include <cluaupp/roblox.hpp>
2
2
 
3
3
  const int STARTING_COINS = 0;
4
+
5
+ int startingCoins();