cluaupp 0.2.3 → 1.0.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.
Files changed (114) hide show
  1. package/CHANGELOG.md +10 -79
  2. package/README.md +42 -88
  3. package/docs/README.md +13 -21
  4. package/docs/architecture.md +13 -76
  5. package/docs/cli.md +12 -64
  6. package/docs/comparison.md +6 -24
  7. package/docs/config.md +6 -7
  8. package/docs/examples/combat.md +19 -227
  9. package/docs/examples/hud.md +13 -81
  10. package/docs/examples/index.md +9 -16
  11. package/docs/getting-started.md +20 -137
  12. package/docs/intellisense.md +5 -41
  13. package/docs/intro.md +16 -15
  14. package/docs/migration.md +38 -0
  15. package/docs/oop/file-tags.md +12 -36
  16. package/docs/oop/index.md +8 -14
  17. package/docs/syntax.md +54 -236
  18. package/editors/vscode/extension.js +1 -1
  19. package/editors/vscode/package.json +4 -4
  20. package/examples/README.md +5 -7
  21. package/examples/game/.vscode/extensions.json +0 -3
  22. package/examples/game/.vscode/settings.json +6 -14
  23. package/examples/game/src/client/hud.client.clpp +54 -0
  24. package/examples/game/src/client/init.client.clpp +6 -0
  25. package/examples/game/src/server/combat.server.clpp +85 -0
  26. package/examples/game/src/server/leaderstats.server.clpp +28 -0
  27. package/examples/game/src/shared/PlayerData.clh +20 -0
  28. package/examples/game/src/shared/config.clp +6 -0
  29. package/examples/game/src/shared/features.clp +88 -0
  30. package/generated/cli.js +40 -24
  31. package/generated/clpp/contract.js +5 -0
  32. package/generated/clpp/index.js +27 -0
  33. package/generated/clpp/paths.js +121 -0
  34. package/generated/clpp/postprocess.js +78 -0
  35. package/generated/clpp/runner.js +102 -0
  36. package/generated/compile.js +26 -138
  37. package/generated/intellisense.js +38 -157
  38. package/generated/libs.js +1 -1
  39. package/generated/lsp.js +1 -8
  40. package/generated/preprocess.js +8 -179
  41. package/generated/system-understander.js +1 -1
  42. package/generated/transpile.js +3 -50
  43. package/generated/utils/project.js +13 -28
  44. package/include/{cluaupp/datatypes.hpp → clpp/datatypes.clh} +1 -1
  45. package/include/{cluaupp/generated/instances.hpp → clpp/generated/instances.clh} +1 -1
  46. package/include/{cluaupp/libs/chrono.hpp → clpp/libs/chrono.clh} +9 -9
  47. package/include/{cluaupp/libs/cmdr.hpp → clpp/libs/cmdr.clh} +4 -4
  48. package/include/{cluaupp/libs/dataservice.hpp → clpp/libs/dataservice.clh} +8 -15
  49. package/include/{cluaupp/libs/display.hpp → clpp/libs/display.clh} +1 -2
  50. package/include/{cluaupp/libs/ezvisual.hpp → clpp/libs/ezvisual.clh} +1 -3
  51. package/include/{cluaupp/libs/formatnumber.hpp → clpp/libs/formatnumber.clh} +2 -3
  52. package/include/{cluaupp/libs/fusion.hpp → clpp/libs/fusion.clh} +5 -5
  53. package/include/{cluaupp/libs/iris.hpp → clpp/libs/iris.clh} +2 -2
  54. package/include/{cluaupp/libs/janitor.hpp → clpp/libs/janitor.clh} +1 -2
  55. package/include/{cluaupp/libs/module3d.hpp → clpp/libs/module3d.clh} +2 -4
  56. package/include/{cluaupp/libs/net.hpp → clpp/libs/net.clh} +2 -4
  57. package/include/{cluaupp/libs/promise.hpp → clpp/libs/promise.clh} +1 -4
  58. package/include/{cluaupp/libs/spring.hpp → clpp/libs/spring.clh} +1 -2
  59. package/include/{cluaupp/libs/statemachine.hpp → clpp/libs/statemachine.clh} +2 -3
  60. package/include/{cluaupp/libs/stickybillboard.hpp → clpp/libs/stickybillboard.clh} +1 -2
  61. package/include/{cluaupp/libs/topbarplus.hpp → clpp/libs/topbarplus.clh} +3 -4
  62. package/include/clpp/libs.clh +24 -0
  63. package/include/clpp/roblox.clh +31 -0
  64. package/package.json +7 -14
  65. package/src/cli.ts +39 -25
  66. package/src/clpp/contract.ts +39 -0
  67. package/src/clpp/index.ts +18 -0
  68. package/src/clpp/paths.ts +117 -0
  69. package/src/clpp/postprocess.ts +93 -0
  70. package/src/clpp/runner.ts +102 -0
  71. package/src/compile.ts +26 -149
  72. package/src/intellisense.ts +36 -165
  73. package/src/libs.ts +1 -1
  74. package/src/lsp.ts +1 -5
  75. package/src/preprocess.ts +8 -180
  76. package/src/system-understander.ts +1 -1
  77. package/src/transpile.ts +9 -52
  78. package/src/utils/project.ts +12 -28
  79. package/templates/game/.vscode/extensions.json +0 -3
  80. package/templates/game/.vscode/settings.json +6 -14
  81. package/templates/game/src/client/init.client.clpp +6 -0
  82. package/templates/game/src/server/leaderstats.server.clpp +29 -0
  83. package/templates/game/src/shared/config.clp +6 -0
  84. package/examples/game/.clangd +0 -25
  85. package/examples/game/compile_flags.txt +0 -6
  86. package/examples/game/src/client/hud.client.cpp +0 -12
  87. package/examples/game/src/client/init.client.cpp +0 -6
  88. package/examples/game/src/server/combat.server.cpp +0 -23
  89. package/examples/game/src/server/leaderstats.server.cpp +0 -29
  90. package/examples/game/src/shared/config.cpp +0 -5
  91. package/examples/game/src/shared/config.h +0 -5
  92. package/include/cluaupp/libs.hpp +0 -25
  93. package/include/cluaupp/roblox.hpp +0 -47
  94. package/src/architecture.ts +0 -500
  95. package/src/ast.ts +0 -37
  96. package/src/emit.ts +0 -609
  97. package/src/emitter/luau-codegen.ts +0 -187
  98. package/src/emitter/translators.ts +0 -168
  99. package/src/headers.ts +0 -233
  100. package/src/layout.ts +0 -114
  101. package/src/lex.ts +0 -158
  102. package/src/parse.ts +0 -729
  103. package/src/parser/collector.ts +0 -148
  104. package/src/parser/index.ts +0 -27
  105. package/src/tree-sitter-cpp.d.ts +0 -4
  106. package/src/understand.ts +0 -322
  107. package/templates/game/src/client/init.client.cpp +0 -6
  108. package/templates/game/src/server/leaderstats.server.cpp +0 -29
  109. package/templates/game/src/shared/config.cpp +0 -5
  110. package/templates/game/src/shared/config.h +0 -5
  111. /package/include/{cluaupp/generated/enums.hpp → clpp/generated/enums.clh} +0 -0
  112. /package/include/{cluaupp/libs/math.hpp → clpp/libs/math.clh} +0 -0
  113. /package/include/{cluaupp/libs/twinkle.hpp → clpp/libs/twinkle.clh} +0 -0
  114. /package/include/{cluaupp/libs/vfx.hpp → clpp/libs/vfx.clh} +0 -0
package/src/compile.ts CHANGED
@@ -1,160 +1,37 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
3
- import { parse } from "./parse.js";
4
- import { emit } from "./emit.js";
5
- import { preprocess, isHeaderFile, toLuauPath, siblingImplementation, siblingHeader, implOutName } from "./preprocess.js";
6
- import { collectLibraries, insertPreamble } from "./libs.js";
7
- import { planOutput } from "./architecture.js";
8
- import { emitHeaderModule, primaryHeaderName } from "./headers.js";
9
- import type { AstNode, AstProgram, ModuleInclude, PreprocessOptions } from "./ast.js";
10
2
  import type { CompileOptions, CompileServiceResult } from "./types.js";
11
-
12
- function shouldAttachRequires(name: string, contents: string): boolean {
13
- const file = String(name || "").replace(/\\/g, "/");
14
- if (/\.json$/i.test(file) || /\.meta\.json$/i.test(file)) {
15
- return false;
16
- }
17
- const trimmed = String(contents || "").trim();
18
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
19
- return false;
20
- }
21
- return true;
22
- }
23
-
24
- function attachRequires(contents: string, source: string, ast: AstProgram, options: PreprocessOptions, outName: string): string {
25
- if (!shouldAttachRequires(outName, contents)) {
26
- return contents;
27
- }
28
- const libraries = String(contents).includes("CluauppLibs") ? [] : collectLibraries(source, ast);
29
- return insertPreamble(contents, options.moduleIncludes || [], libraries, outName);
30
- }
31
-
32
- function posixRel(from: string, file: string): string {
33
- return path.relative(from, file).replace(/\\/g, "/");
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
-
76
- function wireHeaderImpl(ast: AstProgram, fileName: string, options: PreprocessOptions): void {
77
- const headerPath = options.filePath || null;
78
- const impl = headerPath ? siblingImplementation(headerPath) : null;
79
- if (!impl) {
80
- return;
81
- }
82
- mergeSiblingImpl(ast, impl, options);
83
- const typeName = primaryHeaderName(ast, options.relativeName || fileName);
84
- const implRel = options.srcDir ? posixRel(options.srcDir, impl) : path.basename(impl).replace(/\\/g, "/");
85
- const implOut = implOutName(implRel);
86
- options.headerImplName = `${typeName}Module`;
87
- options.moduleIncludes = options.moduleIncludes || [];
88
- if (!options.moduleIncludes.some((spec: ModuleInclude) => spec.name === options.headerImplName)) {
89
- options.moduleIncludes.push({
90
- name: options.headerImplName,
91
- outRel: implOut,
92
- exports: { consts: [], structs: [] },
93
- });
94
- }
95
- }
96
-
97
- function compileHeader(source: string, fileName: string, options: PreprocessOptions, ast: AstProgram): CompileServiceResult {
98
- wireHeaderImpl(ast, fileName, options);
99
- const rel = (options.relativeName || fileName || "header.h").replace(/\\/g, "/");
100
- const outName = toLuauPath(rel);
101
- const luau = attachRequires(emitHeaderModule(ast, options), source, ast, options, outName);
102
- return {
103
- kind: "module",
104
- plan: { kind: "module", strict: options.strict === true },
105
- files: [{ name: outName, contents: luau }],
106
- stale: [],
107
- };
108
- }
3
+ import { compileViaClpp } from "./clpp/runner.js";
4
+ import { clppLuauToGame, shouldSkipInit } from "./clpp/postprocess.js";
5
+ import { emitKind, siblingHeader, toLuauPath } from "./clpp/paths.js";
109
6
 
110
7
  export function compileSource(source: string, fileName?: string, options: CompileOptions = {}): string {
111
- const opts = options as PreprocessOptions;
112
- opts.moduleIncludes = opts.moduleIncludes || [];
113
- const prepared = preprocess(source, opts.filePath || null, opts);
114
- const ast = parse(prepared, fileName || "input.cpp");
115
- if (isHeaderFile(fileName || "") || isHeaderFile(opts.filePath || "")) {
116
- return compileHeader(source, fileName || "header.h", opts, ast).files[0].contents;
117
- }
118
- const luau = emit(ast, opts);
119
- return attachRequires(luau, source, ast, opts, opts.outName || fileName || "input.cpp");
8
+ return compileService(source, fileName, options).files[0]?.contents ?? "";
120
9
  }
121
10
 
122
11
  export function compileService(source: string, fileName?: string, options: CompileOptions = {}): CompileServiceResult {
123
- const opts = options as PreprocessOptions;
124
- opts.moduleIncludes = opts.moduleIncludes || [];
125
- const prepared = preprocess(source, opts.filePath || null, opts);
126
- const ast = parse(prepared, fileName || "input.cpp");
127
- if (isHeaderFile(fileName || "") || isHeaderFile(opts.filePath || "") || isHeaderFile(opts.relativeName || "")) {
128
- return compileHeader(source, fileName || "header.h", opts, ast);
129
- }
130
- const implHeader = opts.filePath ? siblingHeader(opts.filePath) : null;
131
- if (implHeader) {
132
- opts.skipInit = true;
133
- }
134
- const planned = planOutput(ast, fileName || "input.cpp", {
135
- ...opts,
136
- relativeName: opts.relativeName || fileName,
137
- source: prepared,
138
- siblingHeader: implHeader,
12
+ const relative = (options.relativeName || fileName || "input.clpp").replace(/\\/g, "/");
13
+ const outName = options.outName || toLuauPath(relative);
14
+ const header = options.filePath ? siblingHeader(options.filePath) : null;
15
+ const skipInit = options.skipInit === true || shouldSkipInit(relative, Boolean(header));
16
+ const diskPath = options.filePath ? path.resolve(options.filePath) : null;
17
+ const artifact = compileViaClpp({
18
+ source,
19
+ fileName: diskPath ? diskPath.replace(/\\/g, "/") : relative,
20
+ strict: options.strict,
21
+ cwd: diskPath ? path.dirname(diskPath) : options.srcDir,
22
+ });
23
+ const luau = clppLuauToGame(artifact, {
24
+ relativeName: relative,
25
+ outName,
26
+ srcDir: options.srcDir,
27
+ rootDir: options.rootDir,
28
+ strict: options.strict,
29
+ skipInit,
139
30
  });
140
- if (planned.files) {
141
- return {
142
- ...planned,
143
- files: planned.files.map((file: { name: string; contents: string }) => ({
144
- ...file,
145
- contents: attachRequires(file.contents, source, ast, opts, file.name),
146
- })),
147
- };
148
- }
149
- const luau = attachRequires(emit(ast, opts), source, ast, opts, planned.outName || opts.outName || fileName || "input.cpp");
150
- const name =
151
- planned.outName ||
152
- opts.outName ||
153
- String(fileName || "input.cpp").replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau");
154
31
  return {
155
- kind: planned.kind || "flat",
156
- plan: planned.plan,
157
- files: [{ name, contents: luau }],
158
- stale: planned.stale || [],
32
+ kind: emitKind(relative),
33
+ plan: { kind: emitKind(relative), strict: options.strict === true },
34
+ files: [{ name: outName, contents: luau }],
35
+ stale: [],
159
36
  };
160
37
  }
@@ -1,12 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { parse } from "./parse.js";
5
- import { preprocess } from "./preprocess.js";
6
- import { findClangPlusPlus, installMarketplaceExtension, ensureLlvm, CLANGD_ID, reportInstall } from "./editor-install.js";
4
+ import { spawnSync } from "node:child_process";
7
5
  import { pkg, projectRoot as PACKAGE_ROOT } from "./package-info.js";
8
6
  import type { ProjectConfig } from "./types.js";
9
- import type { PreprocessOptions } from "./ast.js";
7
+ import { CLPP_INSTALL_HINT } from "./clpp/contract.js";
8
+ import { compileViaClpp, hasClpp } from "./clpp/runner.js";
9
+ import { isSourceFile } from "./clpp/paths.js";
10
10
 
11
11
  export type CluauppDiagnostic = {
12
12
  line: number;
@@ -15,57 +15,6 @@ export type CluauppDiagnostic = {
15
15
  severity: "error";
16
16
  };
17
17
 
18
- function posix(file: string): string {
19
- return String(file).replace(/\\/g, "/");
20
- }
21
-
22
- function collectFiles(dir: string, files: string[] = []): string[] {
23
- if (!fs.existsSync(dir)) {
24
- return files;
25
- }
26
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
27
- const full = path.join(dir, entry.name);
28
- if (entry.isDirectory()) {
29
- collectFiles(full, files);
30
- } else {
31
- files.push(full);
32
- }
33
- }
34
- return files;
35
- }
36
-
37
- function isCppFile(file: string): boolean {
38
- return /\.(cpp|cc|cxx|c|h|hpp|hh)$/i.test(file);
39
- }
40
-
41
- function clangCompileArgs(root: string): string[] {
42
- const absRoot = posix(path.resolve(root));
43
- const absInclude = posix(path.join(absRoot, "include"));
44
- const absSrc = posix(path.join(absRoot, "src"));
45
- const roblox = posix(path.join(absInclude, "cluaupp", "roblox.hpp"));
46
- return [
47
- "-xc++",
48
- "-std=c++20",
49
- "-ferror-limit=0",
50
- "-I" + absInclude,
51
- "-I" + absSrc,
52
- "-Iinclude",
53
- "-Isrc",
54
- "-include",
55
- roblox,
56
- ];
57
- }
58
-
59
- function compileCommandEntry(root: string, file: string) {
60
- const absRoot = posix(path.resolve(root));
61
- const absFile = posix(path.resolve(file));
62
- return {
63
- directory: absRoot,
64
- file: absFile,
65
- arguments: [clangPath(), ...clangCompileArgs(root), "-c", absFile],
66
- };
67
- }
68
-
69
18
  function copyDir(from: string, to: string): void {
70
19
  fs.mkdirSync(to, { recursive: true });
71
20
  for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
@@ -96,17 +45,6 @@ function uniqueStrings(...lists: unknown[]): string[] {
96
45
  return out;
97
46
  }
98
47
 
99
- function stripCppToolsSettings(current: Record<string, unknown>): Record<string, unknown> {
100
- const next: Record<string, unknown> = {};
101
- for (const [key, value] of Object.entries(current)) {
102
- if (key === "C_Cpp" || key.startsWith("C_Cpp.")) {
103
- continue;
104
- }
105
- next[key] = value;
106
- }
107
- return next;
108
- }
109
-
110
48
  function readJsonObject(file: string): Record<string, unknown> {
111
49
  if (!fs.existsSync(file)) {
112
50
  return {};
@@ -117,7 +55,7 @@ function readJsonObject(file: string): Record<string, unknown> {
117
55
  return parsed as Record<string, unknown>;
118
56
  }
119
57
  } catch {
120
- // ignore corrupt editor json
58
+ // ignore
121
59
  }
122
60
  return {};
123
61
  }
@@ -131,62 +69,6 @@ function writeJson(file: string, value: Record<string, unknown>): void {
131
69
  fs.writeFileSync(file, json, "utf8");
132
70
  }
133
71
 
134
- function clangdEditorArgs(): string[] {
135
- return [
136
- "--compile-commands-dir=${workspaceFolder}",
137
- "--header-insertion=never",
138
- "--query-driver=**/clang++*,**/clang++.exe,**/g++*,**/g++.exe",
139
- ];
140
- }
141
-
142
- export function clangPath(): string {
143
- return findClangPlusPlus() || "clang++";
144
- }
145
-
146
- export function writeCompileCommands(root: string, srcDir: string): boolean {
147
- const files = collectFiles(srcDir).filter((file) => isCppFile(file)).sort();
148
- const json = `${JSON.stringify(files.map((file) => compileCommandEntry(root, file)), null, "\t")}\n`;
149
- const dest = path.join(root, "compile_commands.json");
150
- if (fs.existsSync(dest) && fs.readFileSync(dest, "utf8") === json) {
151
- return false;
152
- }
153
- fs.writeFileSync(dest, json, "utf8");
154
- return true;
155
- }
156
-
157
- export function writeClangd(root: string): boolean {
158
- const flags = clangCompileArgs(root)
159
- .map((flag) => ` - ${flag}`)
160
- .join("\n");
161
- const contents = `CompileFlags:
162
- CompilationDatabase: .
163
- Add:
164
- ${flags}
165
-
166
- Diagnostics:
167
- Suppress: '*'
168
-
169
- ---
170
- If:
171
- PathMatch: .*\\.(cpp|cc|cxx|c|h|hpp|hh)$
172
- CompileFlags:
173
- Add:
174
- - -xc++
175
-
176
- ---
177
- If:
178
- PathMatch: (out|libs)/.*
179
- Index:
180
- Background: Skip
181
- `;
182
- const dest = path.join(root, ".clangd");
183
- if (fs.existsSync(dest) && fs.readFileSync(dest, "utf8") === contents) {
184
- return false;
185
- }
186
- fs.writeFileSync(dest, contents, "utf8");
187
- return true;
188
- }
189
-
190
72
  function writeVscode(root: string): void {
191
73
  const settingsPath = path.join(root, ".vscode", "settings.json");
192
74
  const current = readJsonObject(settingsPath);
@@ -194,19 +76,15 @@ function writeVscode(root: string): void {
194
76
  ...((current["files.associations"] && typeof current["files.associations"] === "object" && !Array.isArray(current["files.associations"]))
195
77
  ? (current["files.associations"] as Record<string, unknown>)
196
78
  : {}),
197
- "*.hpp": "cpp",
198
- "*.h": "cpp",
199
- "*.server.cpp": "cpp",
200
- "*.client.cpp": "cpp",
201
- "*.plugin.cpp": "cpp",
202
- "*.legacy.cpp": "cpp",
203
- "*.legacy.server.cpp": "cpp",
204
- "*.legacy.client.cpp": "cpp",
79
+ "*.clpp": "clpp",
80
+ "*.clp": "clpp",
81
+ "*.clh": "clpp",
82
+ "*.server.clpp": "clpp",
83
+ "*.client.clpp": "clpp",
84
+ "*.plugin.clpp": "clpp",
205
85
  };
206
86
  writeJson(settingsPath, {
207
- ...stripCppToolsSettings(current),
208
- "clangd.enable": true,
209
- "clangd.arguments": clangdEditorArgs(),
87
+ ...current,
210
88
  "files.associations": associations,
211
89
  });
212
90
 
@@ -214,44 +92,30 @@ function writeVscode(root: string): void {
214
92
  const extensions = readJsonObject(extensionsPath);
215
93
  writeJson(extensionsPath, {
216
94
  ...extensions,
217
- recommendations: uniqueStrings(extensions.recommendations, ["llvm-vs-code-extensions.vscode-clangd"]),
95
+ recommendations: uniqueStrings(extensions.recommendations),
218
96
  unwantedRecommendations: uniqueStrings(extensions.unwantedRecommendations, ["ms-vscode.cpptools"]),
219
97
  });
220
-
221
- const props = path.join(root, ".vscode", "c_cpp_properties.json");
222
- if (fs.existsSync(props)) {
223
- fs.unlinkSync(props);
224
- }
225
98
  }
226
99
 
227
- export function diagnosticsFor(source: string, fileName?: string, options: PreprocessOptions = {}): CluauppDiagnostic[] {
100
+ export function diagnosticsFor(source: string, fileName?: string): CluauppDiagnostic[] {
101
+ if (!hasClpp()) {
102
+ return [];
103
+ }
228
104
  try {
229
- const prepared = options.filePath ? preprocess(source, options.filePath, options) : source;
230
- parse(prepared, fileName || "input.cpp");
105
+ compileViaClpp({ source, fileName: fileName || "input.clpp" });
231
106
  return [];
232
107
  } catch (err) {
233
- const error = err as { message?: string; line?: number; col?: number };
234
- const message = error.message ? String(error.message) : "parse error";
108
+ const message = err instanceof Error ? err.message : String(err);
235
109
  const match = message.match(/:(\d+):(\d+):\s*(.*)$/);
236
110
  if (match) {
237
111
  return [{ line: Number(match[1]), col: Number(match[2]), message: match[3], severity: "error" }];
238
112
  }
239
- return [{ line: error.line || 1, col: error.col || 1, message, severity: "error" }];
113
+ return [{ line: 1, col: 1, message, severity: "error" }];
240
114
  }
241
115
  }
242
116
 
243
- export function syncEditorSupport(root: string, config: Partial<ProjectConfig> = {}): void {
244
- const srcDir = path.join(root, config.rootDir || "src");
245
- if (fs.existsSync(srcDir)) {
246
- writeCompileCommands(root, srcDir);
247
- }
248
- writeClangd(root);
117
+ export function syncEditorSupport(root: string, _config: Partial<ProjectConfig> = {}): void {
249
118
  writeVscode(root);
250
- const flags = ["-xc++", "-std=c++20", "-ferror-limit=0", "-Iinclude", "-Isrc", "-include", "include/cluaupp/roblox.hpp"].join("\n") + "\n";
251
- const flagsFile = path.join(root, "compile_flags.txt");
252
- if (!fs.existsSync(flagsFile) || fs.readFileSync(flagsFile, "utf8") !== flags) {
253
- fs.writeFileSync(flagsFile, flags, "utf8");
254
- }
255
119
  }
256
120
 
257
121
  export function installEditorExtension(cluauppRoot = PACKAGE_ROOT): string[] {
@@ -275,12 +139,19 @@ export function installEditorExtension(cluauppRoot = PACKAGE_ROOT): string[] {
275
139
  return installed;
276
140
  }
277
141
 
278
- export async function installEditorSupport(cluauppRoot = PACKAGE_ROOT) {
279
- const local = installEditorExtension(cluauppRoot);
280
- const llvm = ensureLlvm();
281
- reportInstall(llvm, "LLVM clang++");
282
- const clangd = await installMarketplaceExtension(CLANGD_ID, "clangd (LLVM)");
283
- reportInstall(clangd, "clangd");
284
- console.log("cluaupp: C++ completion is clangd — reload the editor (Ctrl+Shift+P → Developer: Reload Window)");
285
- return { local, llvm, clangd };
142
+ export async function installEditorSupport() {
143
+ const local = installEditorExtension();
144
+ if (hasClpp()) {
145
+ const result = spawnSync("clpp", ["install"], { encoding: "utf8", windowsHide: true });
146
+ if (result.status === 0) {
147
+ console.log(String(result.stderr || result.stdout || "clpp install ok").trim());
148
+ } else {
149
+ console.log("cluaupp:", CLPP_INSTALL_HINT);
150
+ }
151
+ } else {
152
+ console.log("cluaupp:", CLPP_INSTALL_HINT);
153
+ }
154
+ return { local };
286
155
  }
156
+
157
+ export const isClppSource = isSourceFile;
package/src/libs.ts CHANGED
@@ -248,7 +248,7 @@ function walk(node, visit) {
248
248
  function modulesFromIncludes(source) {
249
249
  const found = new Set();
250
250
  for (const line of String(source).split(/\r?\n/)) {
251
- const match = line.match(/^\s*#\s*include\s+<cluaupp\/(?:libs\/)?([A-Za-z0-9]+)\.hpp>/);
251
+ const match = line.match(/^\s*#\s*include\s+<(?:clpp|cluaupp)\/(?:libs\/)?([A-Za-z0-9]+)\.(?:clh|hpp)>/);
252
252
  if (!match) {
253
253
  continue;
254
254
  }
package/src/lsp.ts CHANGED
@@ -1,4 +1,3 @@
1
- import path from "node:path";
2
1
  import {
3
2
  createConnection,
4
3
  ProposedFeatures,
@@ -31,10 +30,7 @@ export function start(options: { projectRoot?: string } = {}): void {
31
30
 
32
31
  const publish = (document: TextDocument) => {
33
32
  const file = uriPath(document.uri);
34
- const items = diagnosticsFor(document.getText(), file, {
35
- filePath: file,
36
- includeDirs: [path.dirname(file), path.join(projectRoot, "src"), path.join(projectRoot, "include")],
37
- });
33
+ const items = diagnosticsFor(document.getText(), file);
38
34
  connection.sendDiagnostics({
39
35
  uri: document.uri,
40
36
  diagnostics: items.map((item) => ({
package/src/preprocess.ts CHANGED
@@ -1,180 +1,8 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import type { ModuleInclude, PreprocessOptions } from "./ast.js";
4
-
5
- export const SOURCE_EXTS = [".cpp", ".cc", ".cxx", ".c", ".h", ".hpp", ".hh"];
6
-
7
- export function isSourceFile(fileName: string): boolean {
8
- return SOURCE_EXTS.includes(path.extname(fileName).toLowerCase());
9
- }
10
-
11
- export function isHeaderFile(fileName: string): boolean {
12
- return [".h", ".hpp", ".hh"].includes(path.extname(fileName).toLowerCase());
13
- }
14
-
15
- export function toLuauPath(filePath: string): string {
16
- return filePath.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau");
17
- }
18
-
19
- export function isEngineStub(filePath: string): boolean {
20
- const normalized = filePath.replace(/\\/g, "/").toLowerCase();
21
- return (
22
- normalized.includes("/include/cluaupp/") ||
23
- normalized.includes("/include/cluau/") ||
24
- normalized.endsWith("/roblox.hpp") ||
25
- normalized.endsWith("/roblox.h")
26
- );
27
- }
28
-
29
- function includeVariants(name: string): string[] {
30
- const variants = [name];
31
- if (/\.h$/i.test(name)) {
32
- variants.push(name.replace(/\.h$/i, ".hpp"), name.replace(/\.h$/i, ".hh"));
33
- } else if (/\.hpp$/i.test(name)) {
34
- variants.push(name.replace(/\.hpp$/i, ".h"), name.replace(/\.hpp$/i, ".hh"));
35
- } else if (/\.hh$/i.test(name)) {
36
- variants.push(name.replace(/\.hh$/i, ".h"), name.replace(/\.hh$/i, ".hpp"));
37
- }
38
- return [...new Set(variants)];
39
- }
40
-
41
- export function siblingImplementation(headerPath: string | null | undefined): string | null {
42
- if (!headerPath || !isHeaderFile(headerPath)) {
43
- return null;
44
- }
45
- const base = headerPath.replace(/\.(h|hpp|hh)$/i, "");
46
- for (const ext of [".cpp", ".cc", ".cxx", ".c"]) {
47
- if (fs.existsSync(base + ext)) {
48
- return base + ext;
49
- }
50
- }
51
- return null;
52
- }
53
-
54
- export function siblingHeader(implPath: string | null | undefined): string | null {
55
- if (!implPath || isHeaderFile(implPath)) {
56
- return null;
57
- }
58
- const base = String(implPath).replace(/\.(cpp|cc|cxx|c)$/i, "");
59
- if (base === String(implPath)) {
60
- return null;
61
- }
62
- for (const ext of [".h", ".hpp", ".hh"]) {
63
- if (fs.existsSync(base + ext)) {
64
- return base + ext;
65
- }
66
- }
67
- return null;
68
- }
69
-
70
- export function implOutName(rel: string): string {
71
- return String(rel)
72
- .replace(/\\/g, "/")
73
- .replace(/\.(cpp|cc|cxx|c)$/i, "Impl.luau");
74
- }
75
-
76
- export function resolveInclude(name: string, fromFile: string, includeDirs?: string[]): string | null {
77
- const bases = [path.dirname(fromFile), ...(includeDirs || [])];
78
- for (const base of bases) {
79
- for (const variant of includeVariants(name)) {
80
- const candidate = path.resolve(base, variant);
81
- if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
82
- return candidate;
83
- }
84
- }
85
- }
86
- return null;
87
- }
88
-
89
- export function scanHeaderExports(source: string): { consts: string[]; structs: string[]; hasProtos: boolean } {
90
- const consts: string[] = [];
91
- const structs: string[] = [];
92
- for (const match of String(source).matchAll(/\bconst\s+(?:int|bool|float|double|auto|string)\s+(\w+)/g)) {
93
- if (!consts.includes(match[1])) {
94
- consts.push(match[1]);
95
- }
96
- }
97
- for (const match of String(source).matchAll(/\bstruct\s+(\w+)/g)) {
98
- if (!structs.includes(match[1])) {
99
- structs.push(match[1]);
100
- }
101
- }
102
- const hasProtos = /\b(?:void|int|bool|float|double|auto|string|[\w:]+)\s+\w+\s*\([^;]*\)\s*;/.test(String(source));
103
- return { consts, structs, hasProtos };
104
- }
105
-
106
- function pushModuleInclude(options: PreprocessOptions, resolved: string, impl: string | null): void {
107
- options.moduleIncludes = options.moduleIncludes || [];
108
- const srcDir = options.srcDir || null;
109
- const moduleFile = impl || resolved;
110
- const rel = srcDir ? path.relative(srcDir, moduleFile).replace(/\\/g, "/") : path.basename(moduleFile);
111
- const headerText = fs.readFileSync(resolved, "utf8");
112
- options.moduleIncludes.push({
113
- name: path.basename(resolved).replace(/\.(h|hpp|hh)$/i, ""),
114
- outRel: rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau"),
115
- header: resolved,
116
- impl: impl || null,
117
- exports: scanHeaderExports(headerText),
118
- });
119
- }
120
-
121
- export function applyPragmas(source: string, options: PreprocessOptions): void {
122
- let nstrict = false;
123
- let strict = false;
124
- for (const line of String(source).split(/\r?\n/)) {
125
- if (/^\s*#\s*pragma\s+nstrict\b/i.test(line)) {
126
- nstrict = true;
127
- } else if (/^\s*#\s*pragma\s+strict\b/i.test(line)) {
128
- strict = true;
129
- }
130
- }
131
- if (nstrict) {
132
- options.strict = false;
133
- } else if (strict) {
134
- options.strict = true;
135
- }
136
- }
137
-
138
- export function preprocess(source: string, filePath: string | null, options: PreprocessOptions = {}): string {
139
- if (!options.pragmaResolved) {
140
- options.pragmaResolved = true;
141
- applyPragmas(source, options);
142
- }
143
- const seen = options.seen || new Set<string>();
144
- options.seen = seen;
145
- const includeDirs = options.includeDirs || [];
146
- const resolvedSelf = filePath ? path.resolve(filePath) : null;
147
- if (resolvedSelf) {
148
- seen.add(resolvedSelf);
149
- }
150
-
151
- const lines = String(source).split(/\r?\n/);
152
- const out: string[] = [];
153
-
154
- for (const line of lines) {
155
- const quoted = line.match(/^\s*#\s*include\s+"([^"]+)"/);
156
- if (quoted) {
157
- const resolved = resolveInclude(quoted[1], filePath || process.cwd(), includeDirs);
158
- if (!resolved || isEngineStub(resolved) || seen.has(resolved)) {
159
- continue;
160
- }
161
- const impl = siblingImplementation(resolved);
162
- const including = filePath ? path.resolve(filePath) : null;
163
- const ownHeader = Boolean(impl && including && path.resolve(impl) === including);
164
- if (!ownHeader) {
165
- pushModuleInclude(options, resolved, impl);
166
- continue;
167
- }
168
- seen.add(resolved);
169
- const inner = fs.readFileSync(resolved, "utf8");
170
- out.push(preprocess(inner, resolved, { ...options, seen }));
171
- continue;
172
- }
173
- if (/^\s*#\s*include\s+</.test(line)) {
174
- continue;
175
- }
176
- out.push(line);
177
- }
178
-
179
- return out.join("\n");
180
- }
1
+ export {
2
+ SOURCE_EXTS,
3
+ isSourceFile,
4
+ isHeaderFile,
5
+ isEngineStub,
6
+ toLuauPath,
7
+ siblingHeader,
8
+ } from "./clpp/paths.js";
@@ -40,7 +40,7 @@ export interface ArchitectureReport {
40
40
  reasoning: string[];
41
41
  }
42
42
 
43
- const EXT = "(cpp|cc|cxx|c|h|hpp|hh)";
43
+ const EXT = "(clpp|clp|clh)";
44
44
  const EXT_REGEX = new RegExp(`\\.${EXT}$`, "i");
45
45
  const SCORE_THRESHOLD = 2;
46
46