cluaupp 1.4.1 → 1.5.1
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 +55 -0
- package/README.md +10 -5
- package/api/CHANGELOG.md +3 -0
- package/api/chat.js +74 -0
- package/api/fixtures/roblox/README.md +6 -0
- package/api/fixtures/roblox/aux/sample.d.luau +6 -0
- package/api/fixtures/roblox/mini/Mini-API-Dump.json +72 -0
- package/api/generated/bench-latest.json +39 -0
- package/api/generated/capability-profile.json +76 -0
- package/api/generated/lsp-index.json +1 -0
- package/api/generated/luau/classes.d.luau +11221 -0
- package/api/generated/luau/datatypes.d.luau +327 -0
- package/api/generated/luau/enums.d.luau +4918 -0
- package/api/generated/luau/globals.d.luau +12 -0
- package/api/manifests/roblox-target.manifest.json +22 -0
- package/api/normalized/roblox-target.profile.json +237461 -0
- package/api/overrides/classes.json +8 -0
- package/api/overrides/compatibility.json +9 -0
- package/api/overrides/datatypes.json +1579 -0
- package/api/overrides/enums.json +8 -0
- package/api/overrides/globals.json +8 -0
- package/api/overrides/type-mappings.json +9 -0
- package/api/roblox-api.lock.json +9 -0
- package/data/Mini-API-Dump.json +1 -0
- package/editors/vscode/README.md +19 -0
- package/editors/vscode/extension.js +222 -21
- package/editors/vscode/icons/axiom.svg +4 -0
- package/editors/vscode/icons/bloom.svg +5 -0
- package/editors/vscode/icons/flare.svg +5 -0
- package/editors/vscode/icons/helm.svg +4 -0
- package/editors/vscode/icons/hive.svg +5 -0
- package/editors/vscode/icons/mint.svg +4 -0
- package/editors/vscode/icons/shift.svg +6 -0
- package/editors/vscode/language-configuration-schema.json +24 -0
- package/editors/vscode/language-configuration.json +21 -0
- package/editors/vscode/package.json +220 -6
- package/editors/vscode/snippets/axiom.json +12 -0
- package/editors/vscode/snippets/bloom.json +12 -0
- package/editors/vscode/snippets/flare.json +27 -0
- package/editors/vscode/snippets/helm.json +12 -0
- package/editors/vscode/snippets/hive.json +12 -0
- package/editors/vscode/snippets/mint.json +12 -0
- package/editors/vscode/snippets/shift.json +17 -0
- package/editors/vscode/syntaxes/axiom.tmLanguage.json +5 -0
- package/editors/vscode/syntaxes/bloom.tmLanguage.json +5 -0
- package/editors/vscode/syntaxes/flare.tmLanguage.json +75 -0
- package/editors/vscode/syntaxes/helm.tmLanguage.json +5 -0
- package/editors/vscode/syntaxes/hive.tmLanguage.json +5 -0
- package/editors/vscode/syntaxes/mint.tmLanguage.json +5 -0
- package/editors/vscode/syntaxes/schema.tmLanguage.json +89 -0
- package/editors/vscode/syntaxes/shift.tmLanguage.json +5 -0
- package/examples/game/.cluaupp/compile-cache/ReplicatedStorage__Shared__Constants__Datas__TemplateData.clh.json +1 -0
- package/examples/game/.cluaupp/compile-cache/ServerScriptService__Boot__DataBoot.server.clpp.json +1 -0
- package/examples/game/.cluaupp/compile-cache/ServerScriptService__Configuration__ServerConfig.clp.json +1 -0
- package/examples/game/.cluaupp/compile-cache/ServerScriptService__Handlers__Guard.server.clpp.json +1 -0
- package/examples/game/.cluaupp/compile-cache/ServerScriptService__Handlers__PlayerHandler.server.clpp.json +1 -0
- package/examples/game/.cluaupp/compile-cache/StarterPlayer__StarterCharacterScripts__Character__CharacterReady.client.clpp.json +1 -0
- package/examples/game/.cluaupp/compile-cache/StarterPlayer__StarterPlayerScripts__Controllers__DataController.client.clpp.json +1 -0
- package/examples/game/.cluaupp/compile-cache/index.json +12 -0
- package/examples/game/.cluaupp/datamodel-profile.json +94 -0
- package/examples/game/.vscode/extensions.json +3 -1
- package/examples/game/.vscode/settings.json +13 -1
- package/examples/game/include/clpp/datatypes.clh +337 -373
- package/examples/game/include/clpp/generated/datamodel.clh +25 -0
- package/examples/game/include/clpp/generated/enums.clh +1 -1
- package/examples/game/include/clpp/generated/instances.clh +41135 -41064
- package/examples/game/include/clpp/libs/axiom.clh +1 -1
- package/examples/game/include/clpp/libs/math.clh +14 -0
- package/examples/game/include/clpp/libs/roster.clh +6 -0
- package/examples/game/include/clpp/libs/spring.clh +17 -0
- package/examples/game/include/clpp/libs/ward.clh +1 -0
- package/examples/game/include/clpp/roblox.clh +3 -4
- package/examples/game/libs/ArrayIndexer/init.luau +209 -0
- package/examples/game/libs/Axiom/init.luau +608 -0
- package/examples/game/libs/Bloom/init.luau +433 -0
- package/examples/game/libs/Coil/init.luau +215 -0
- package/examples/game/libs/Crest/init.luau +426 -0
- package/examples/game/libs/Echo/init.luau +417 -0
- package/examples/game/libs/Ember/init.luau +365 -0
- package/examples/game/libs/Flare/init.luau +717 -0
- package/examples/game/libs/Gleam/init.luau +729 -0
- package/examples/game/libs/Guide/init.luau +215 -0
- package/examples/game/libs/Helm/init.luau +377 -0
- package/examples/game/libs/Hive/init.luau +241 -0
- package/examples/game/libs/Keep/Client.luau +114 -0
- package/examples/game/libs/Keep/Data.luau +883 -0
- package/examples/game/libs/Keep/Engine.luau +211 -0
- package/examples/game/libs/Keep/Enum.luau +6 -0
- package/examples/game/libs/Keep/Enums.luau +10 -0
- package/examples/game/libs/Keep/Path.luau +236 -0
- package/examples/game/libs/Keep/Replicator.luau +298 -0
- package/examples/game/libs/Keep/Server.luau +251 -0
- package/examples/game/libs/Keep/Store.luau +652 -0
- package/examples/game/libs/Keep/Trade.luau +455 -0
- package/examples/game/libs/Keep/init.luau +205 -0
- package/examples/game/libs/Lens/init.luau +609 -0
- package/examples/game/libs/Mint/init.luau +279 -0
- package/examples/game/libs/Net/init.luau +156 -0
- package/examples/game/libs/Net/src/codec.luau +224 -0
- package/examples/game/libs/Occlude/init.luau +117 -0
- package/examples/game/libs/Pin/init.luau +309 -0
- package/examples/game/libs/Promise/LICENSE +21 -0
- package/examples/game/libs/Promise/_impl/LICENSE +21 -0
- package/examples/game/libs/Promise/_impl/init.lua +1991 -0
- package/examples/game/libs/Promise/init.lua +1990 -0
- package/examples/game/libs/Promise/init.luau +13 -0
- package/examples/game/libs/Promise/types.luau +46 -0
- package/examples/game/libs/Roster/init.luau +686 -0
- package/examples/game/libs/SOURCES.md +31 -0
- package/examples/game/libs/Shift/init.luau +238 -0
- package/examples/game/libs/Spark/init.luau +487 -0
- package/examples/game/libs/Stage/init.luau +178 -0
- package/examples/game/libs/Sweep/init.luau +259 -0
- package/examples/game/libs/Trace/init.luau +213 -0
- package/examples/game/libs/Ward/init.luau +211 -0
- package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau +29 -0
- package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau.map.json +13 -0
- package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau +83 -0
- package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau.map.json +13 -0
- package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau +18 -0
- package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau.map.json +13 -0
- package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau +14 -0
- package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau.map.json +13 -0
- package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau +15 -0
- package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau.map.json +13 -0
- package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau +50 -0
- package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau.map.json +13 -0
- package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau +12 -0
- package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau.map.json +13 -0
- package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau +16 -0
- package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau.map.json +13 -0
- package/examples/game/src/ReplicatedStorage/Shared/Net/Net.clh +26 -0
- package/examples/game/src/ReplicatedStorage/Shared/Net/Net.flare +5 -0
- package/examples/game/src/ServerScriptService/Boot/DataBoot.server.clpp +2 -2
- package/examples/game/src/ServerScriptService/Handlers/PlayerHandler.server.clpp +1 -1
- package/examples/game/stylua.toml +9 -0
- package/generated/api/build-profile.js +115 -0
- package/generated/api/cache.js +44 -0
- package/generated/api/coverage.js +32 -0
- package/generated/api/diff.js +125 -0
- package/generated/api/generators/clpp-headers.js +353 -0
- package/generated/api/generators/docs-reference.js +7 -0
- package/generated/api/generators/lsp-index.js +81 -0
- package/generated/api/generators/luau-defs.js +94 -0
- package/generated/api/ide.js +76 -0
- package/generated/api/identifier.js +47 -0
- package/generated/api/index.js +324 -0
- package/generated/api/loader.js +57 -0
- package/generated/api/merge.js +88 -0
- package/generated/api/model.js +5 -0
- package/generated/api/normalize.js +231 -0
- package/generated/api/overrides-validate.js +58 -0
- package/generated/api/perf.js +12 -0
- package/generated/api/registry.js +136 -0
- package/generated/api/release.js +44 -0
- package/generated/api/resolver.js +250 -0
- package/generated/api/sources/aux-types-diff.js +75 -0
- package/generated/api/sources/client-tracker.js +11 -0
- package/generated/api/sources/creator-docs.js +42 -0
- package/generated/api/sources/luau-types.js +34 -0
- package/generated/api/sources/mini-dump.js +17 -0
- package/generated/api/sources/studio-api.js +8 -0
- package/generated/api/sources/studio-apiv2.js +11 -0
- package/generated/api/sources/studio-full-api.js +8 -0
- package/generated/api/studio-smoke.js +80 -0
- package/generated/api/type-parse.js +49 -0
- package/generated/api/understander.js +51 -0
- package/generated/api/validate.js +135 -0
- package/generated/api-cli.js +421 -0
- package/generated/cli.js +17 -6
- package/generated/clpp/contract.js +3 -3
- package/generated/clpp/format-luau.js +236 -0
- package/generated/clpp/index.js +13 -1
- package/generated/clpp/modules.js +227 -0
- package/generated/clpp/postprocess.js +86 -5
- package/generated/clpp/rewrite-janitor.js +145 -0
- package/generated/clpp/runner.js +14 -2
- package/generated/compile.js +82 -0
- package/generated/flare/index.js +15 -3
- package/generated/flare/language.js +561 -0
- package/generated/flare/parse.js +19 -7
- package/generated/intellisense.js +76 -2
- package/generated/lsp.js +118 -7
- package/generated/native/index.js +217 -16
- package/generated/target/authority-check.js +136 -0
- package/generated/target/authority-graph.js +74 -0
- package/generated/target/build-cache.js +112 -0
- package/generated/target/capabilities.js +89 -0
- package/generated/target/capability-profile.js +30 -0
- package/generated/target/component-contracts.js +60 -0
- package/generated/target/context-check.js +160 -0
- package/generated/target/datamodel-check.js +170 -0
- package/generated/target/datamodel-generate.js +74 -0
- package/generated/target/datamodel.js +233 -0
- package/generated/target/diagnostics.js +63 -0
- package/generated/target/doctor.js +152 -0
- package/generated/target/flare-version.js +58 -0
- package/generated/target/get-service.js +69 -0
- package/generated/target/lifetime-check.js +47 -0
- package/generated/target/luau-stamp.js +39 -0
- package/generated/target/optimizer-advise.js +88 -0
- package/generated/target/optimizer-apply.js +44 -0
- package/generated/target/optimizer-soa.js +254 -0
- package/generated/target/parallel-check.js +136 -0
- package/generated/target/pgo.js +148 -0
- package/generated/target/project-docs.js +69 -0
- package/generated/target/project-graph.js +71 -0
- package/generated/target/roblox.js +63 -0
- package/generated/target/security-check.js +41 -0
- package/generated/target/source-map.js +25 -0
- package/generated/target/studio-bridge.js +112 -0
- package/generated/utils/project.js +178 -8
- package/generated/utils/rojo-mapper.js +11 -3
- package/include/clpp/datatypes.clh +337 -373
- package/include/clpp/generated/enums.clh +1 -1
- package/include/clpp/generated/instances.clh +41135 -41064
- package/include/clpp/libs/axiom.clh +1 -1
- package/include/clpp/libs/hive.clh +1 -0
- package/include/clpp/libs/keep.clh +2 -0
- package/include/clpp/libs/math.clh +14 -0
- package/include/clpp/libs/roster.clh +6 -0
- package/include/clpp/libs/spring.clh +17 -0
- package/include/clpp/libs/ward.clh +1 -0
- package/include/clpp/roblox.clh +3 -4
- package/package.json +10 -3
- package/plugins/CluauppNav/Plugin.luau +207 -0
- package/plugins/CluauppNav/ProfileLogger.luau +27 -0
- package/plugins/CluauppNav/README.md +24 -0
- package/runtime/Axiom/init.luau +10 -7
- package/runtime/Coil/init.luau +8 -2
- package/runtime/Crest/init.luau +1 -0
- package/runtime/Flare/init.luau +2 -1
- package/runtime/Gleam/init.luau +15 -4
- package/runtime/Hive/init.luau +36 -14
- package/runtime/Pin/init.luau +5 -2
- package/runtime/Roster/init.luau +8 -0
- package/runtime/Spark/init.luau +3 -0
- package/runtime/Sweep/init.luau +2 -0
- package/runtime/Trace/init.luau +3 -0
- package/runtime/Ward/init.luau +38 -5
- package/scripts/postinstall-editor.js +17 -0
- package/skills/README.md +13 -0
- package/skills/clpp-hive-flare/SKILL.md +79 -0
- package/skills/clpp-language/SKILL.md +123 -0
- package/skills/clpp-language/compiler.md +89 -0
- package/skills/clpp-style/SKILL.md +95 -0
- package/src/api/build-profile.ts +138 -0
- package/src/api/cache.ts +45 -0
- package/src/api/coverage.ts +46 -0
- package/src/api/diff.ts +143 -0
- package/src/api/generators/clpp-headers.ts +377 -0
- package/src/api/generators/docs-reference.ts +11 -0
- package/src/api/generators/lsp-index.ts +106 -0
- package/src/api/generators/luau-defs.ts +98 -0
- package/src/api/ide.ts +87 -0
- package/src/api/identifier.ts +47 -0
- package/src/api/index.ts +269 -0
- package/src/api/loader.ts +60 -0
- package/src/api/merge.ts +126 -0
- package/src/api/model.ts +283 -0
- package/src/api/normalize.ts +257 -0
- package/src/api/overrides-validate.ts +58 -0
- package/src/api/perf.ts +15 -0
- package/src/api/registry.ts +162 -0
- package/src/api/release.ts +47 -0
- package/src/api/resolver.ts +255 -0
- package/src/api/sources/aux-types-diff.ts +81 -0
- package/src/api/sources/client-tracker.ts +10 -0
- package/src/api/sources/creator-docs.ts +37 -0
- package/src/api/sources/luau-types.ts +30 -0
- package/src/api/sources/mini-dump.ts +50 -0
- package/src/api/sources/studio-api.ts +7 -0
- package/src/api/sources/studio-apiv2.ts +10 -0
- package/src/api/sources/studio-full-api.ts +7 -0
- package/src/api/studio-smoke.ts +82 -0
- package/src/api/type-parse.ts +48 -0
- package/src/api/understander.ts +72 -0
- package/src/api/validate.ts +160 -0
- package/src/api-cli.ts +493 -0
- package/src/api.generated.js +37 -22
- package/src/cli.ts +22 -7
- package/src/clpp/contract.ts +34 -3
- package/src/clpp/format-luau.ts +262 -0
- package/src/clpp/index.ts +13 -2
- package/src/clpp/modules.ts +252 -0
- package/src/clpp/postprocess.ts +100 -5
- package/src/clpp/rewrite-janitor.ts +156 -0
- package/src/clpp/runner.ts +14 -2
- package/src/compile.ts +87 -0
- package/src/flare/index.ts +20 -1
- package/src/flare/language.ts +708 -0
- package/src/flare/parse.ts +20 -7
- package/src/flare/types.ts +2 -0
- package/src/intellisense.ts +76 -2
- package/src/lsp.ts +151 -8
- package/src/native/index.ts +225 -16
- package/src/target/authority-check.ts +162 -0
- package/src/target/authority-graph.ts +80 -0
- package/src/target/build-cache.ts +129 -0
- package/src/target/capabilities.ts +115 -0
- package/src/target/capability-profile.ts +25 -0
- package/src/target/component-contracts.ts +64 -0
- package/src/target/context-check.ts +177 -0
- package/src/target/datamodel-check.ts +188 -0
- package/src/target/datamodel-generate.ts +78 -0
- package/src/target/datamodel.ts +261 -0
- package/src/target/diagnostics.ts +67 -0
- package/src/target/doctor.ts +171 -0
- package/src/target/flare-version.ts +61 -0
- package/src/target/get-service.ts +85 -0
- package/src/target/lifetime-check.ts +45 -0
- package/src/target/luau-stamp.ts +33 -0
- package/src/target/optimizer-advise.ts +93 -0
- package/src/target/optimizer-apply.ts +63 -0
- package/src/target/optimizer-soa.ts +270 -0
- package/src/target/parallel-check.ts +135 -0
- package/src/target/pgo.ts +157 -0
- package/src/target/project-docs.ts +68 -0
- package/src/target/project-graph.ts +75 -0
- package/src/target/roblox.ts +78 -0
- package/src/target/security-check.ts +40 -0
- package/src/target/source-map.ts +36 -0
- package/src/target/studio-bridge.ts +140 -0
- package/src/types.ts +14 -0
- package/src/utils/project.ts +183 -9
- package/src/utils/rojo-mapper.ts +12 -3
- package/stylua.toml +9 -0
- package/templates/game/.vscode/extensions.json +3 -1
- package/templates/game/.vscode/settings.json +8 -1
- package/templates/game/include/clpp/datatypes.clh +337 -373
- package/templates/game/include/clpp/generated/enums.clh +1 -1
- package/templates/game/include/clpp/generated/instances.clh +41135 -41064
- package/templates/game/include/clpp/libs/axiom.clh +1 -1
- package/templates/game/include/clpp/libs/roster.clh +6 -0
- package/templates/game/include/clpp/libs/ward.clh +1 -0
- package/templates/game/include/clpp/roblox.clh +3 -4
- package/templates/game/src/ReplicatedStorage/Shared/Net/Net.flare +5 -0
- package/templates/game/src/ServerScriptService/Boot/DataBoot.server.clpp +2 -2
- package/templates/game/src/ServerScriptService/Handlers/PlayerHandler.server.clpp +1 -1
- package/templates/game/stylua.toml +9 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan CL++ source for Context Safety violations (capability rules).
|
|
3
|
+
* Heuristic line scan — not a full AST. Good enough for phase-1 platform rules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CompileDiagnostic } from "../clpp/contract.js";
|
|
7
|
+
import { builtinCapabilityProfile, type CapabilityProfile, type CapabilityRule } from "./capabilities.js";
|
|
8
|
+
import { runContextFromFileName, type RunContext } from "./roblox.js";
|
|
9
|
+
|
|
10
|
+
export interface ContextViolation extends CompileDiagnostic {
|
|
11
|
+
code: string;
|
|
12
|
+
context: RunContext;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Strip line comments, block comments, and rough string/char literals for scanning. */
|
|
16
|
+
export function stripNoise(source: string): string {
|
|
17
|
+
let out = "";
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < source.length) {
|
|
20
|
+
const c = source[i];
|
|
21
|
+
const next = source[i + 1];
|
|
22
|
+
if (c === "/" && next === "/") {
|
|
23
|
+
while (i < source.length && source[i] !== "\n") {
|
|
24
|
+
i++;
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (c === "/" && next === "*") {
|
|
29
|
+
i += 2;
|
|
30
|
+
while (i < source.length - 1 && !(source[i] === "*" && source[i + 1] === "/")) {
|
|
31
|
+
if (source[i] === "\n") {
|
|
32
|
+
out += "\n";
|
|
33
|
+
}
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
i += 2;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
40
|
+
const quote = c;
|
|
41
|
+
out += " ";
|
|
42
|
+
i++;
|
|
43
|
+
while (i < source.length && source[i] !== quote) {
|
|
44
|
+
if (source[i] === "\\" && i + 1 < source.length) {
|
|
45
|
+
i += 2;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (source[i] === "\n") {
|
|
49
|
+
out += "\n";
|
|
50
|
+
}
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
i++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
out += c;
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function lineColumn(source: string, index: number): { line: number; column: number } {
|
|
63
|
+
let line = 1;
|
|
64
|
+
let column = 1;
|
|
65
|
+
for (let i = 0; i < index && i < source.length; i++) {
|
|
66
|
+
if (source[i] === "\n") {
|
|
67
|
+
line++;
|
|
68
|
+
column = 1;
|
|
69
|
+
} else {
|
|
70
|
+
column++;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { line, column };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function activeRegionContext(lineText: string, current: RunContext, fileContext: RunContext): RunContext {
|
|
77
|
+
const trimmed = lineText.trim();
|
|
78
|
+
if (/^\[\[\s*server\s*\]\]/i.test(trimmed)) {
|
|
79
|
+
return "Server";
|
|
80
|
+
}
|
|
81
|
+
if (/^\[\[\s*client\s*\]\]/i.test(trimmed)) {
|
|
82
|
+
return "Client";
|
|
83
|
+
}
|
|
84
|
+
if (/^\[\[\s*shared\s*\]\]/i.test(trimmed) || /^\[\[\s*plugin\s*\]\]/i.test(trimmed)) {
|
|
85
|
+
if (/plugin/i.test(trimmed)) {
|
|
86
|
+
return "Plugin";
|
|
87
|
+
}
|
|
88
|
+
return "Shared";
|
|
89
|
+
}
|
|
90
|
+
return current || fileContext;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function checkContextSafety(
|
|
94
|
+
source: string,
|
|
95
|
+
fileName: string,
|
|
96
|
+
profile: CapabilityProfile = builtinCapabilityProfile(),
|
|
97
|
+
): ContextViolation[] {
|
|
98
|
+
const fileContext = runContextFromFileName(fileName);
|
|
99
|
+
const cleaned = stripNoise(source);
|
|
100
|
+
const violations: ContextViolation[] = [];
|
|
101
|
+
const lines = cleaned.split(/\r?\n/);
|
|
102
|
+
let region: RunContext = fileContext;
|
|
103
|
+
|
|
104
|
+
const compiled = profile.rules.map((rule) => ({
|
|
105
|
+
rule,
|
|
106
|
+
re: new RegExp(rule.pattern, "g"),
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
let offset = 0;
|
|
110
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
111
|
+
const lineText = lines[lineIndex];
|
|
112
|
+
region = activeRegionContext(lineText, region, fileContext);
|
|
113
|
+
|
|
114
|
+
for (const { rule, re } of compiled) {
|
|
115
|
+
if (!rule.forbiddenIn.includes(region)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
re.lastIndex = 0;
|
|
119
|
+
let match: RegExpExecArray | null;
|
|
120
|
+
while ((match = re.exec(lineText))) {
|
|
121
|
+
const abs = offset + (match.index || 0);
|
|
122
|
+
const pos = lineColumn(source, mapCleanIndexToSource(source, cleaned, abs));
|
|
123
|
+
violations.push({
|
|
124
|
+
code: rule.id,
|
|
125
|
+
message: `${rule.id}: ${rule.message} (context=${region})`,
|
|
126
|
+
line: pos.line,
|
|
127
|
+
column: pos.column,
|
|
128
|
+
severity: rule.severity,
|
|
129
|
+
context: region,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
offset += lineText.length + 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return violations;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Best-effort map from cleaned index back toward original (same newlines preserved). */
|
|
140
|
+
function mapCleanIndexToSource(original: string, cleaned: string, cleanIndex: number): number {
|
|
141
|
+
if (cleaned.length === original.length) {
|
|
142
|
+
return Math.min(cleanIndex, original.length);
|
|
143
|
+
}
|
|
144
|
+
// Newlines aligned — walk both counting newlines + approx columns
|
|
145
|
+
let oi = 0;
|
|
146
|
+
let ci = 0;
|
|
147
|
+
while (ci < cleanIndex && oi < original.length && ci < cleaned.length) {
|
|
148
|
+
if (original[oi] === cleaned[ci]) {
|
|
149
|
+
oi++;
|
|
150
|
+
ci++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
// skip noise already stripped from cleaned
|
|
154
|
+
oi++;
|
|
155
|
+
}
|
|
156
|
+
return oi;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function formatContextViolations(fileName: string, violations: ContextViolation[]): string {
|
|
160
|
+
return violations
|
|
161
|
+
.map((v) => `${fileName}:${v.line}:${v.column}: ${v.severity}[${v.code}]: ${v.message}`)
|
|
162
|
+
.join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function assertContextSafe(source: string, fileName: string, profile?: CapabilityProfile): void {
|
|
166
|
+
const all = checkContextSafety(source, fileName, profile);
|
|
167
|
+
const errors = all.filter((v) => v.severity === "error");
|
|
168
|
+
if (errors.length) {
|
|
169
|
+
throw new Error(formatContextViolations(fileName, errors));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function listContextWarnings(source: string, fileName: string, profile?: CapabilityProfile): ContextViolation[] {
|
|
174
|
+
return checkContextSafety(source, fileName, profile).filter((v) => v.severity === "warning");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type { CapabilityRule };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan CL++ for WaitForChild / FindFirstChild chains against the Typed DataModel graph.
|
|
3
|
+
* Supports GetService roots and simple `auto x = …WaitForChild` variable binding.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CompileDiagnostic } from "../clpp/contract.js";
|
|
7
|
+
import type { DatamodelNode, DatamodelProfile } from "./datamodel.js";
|
|
8
|
+
import { lookupChild } from "./datamodel.js";
|
|
9
|
+
|
|
10
|
+
export interface DatamodelViolation extends CompileDiagnostic {
|
|
11
|
+
code: string;
|
|
12
|
+
pathHint?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function stripComments(source: string): string {
|
|
16
|
+
let out = "";
|
|
17
|
+
let i = 0;
|
|
18
|
+
while (i < source.length) {
|
|
19
|
+
const c = source[i];
|
|
20
|
+
const next = source[i + 1];
|
|
21
|
+
if (c === "/" && next === "/") {
|
|
22
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (c === "/" && next === "*") {
|
|
26
|
+
i += 2;
|
|
27
|
+
while (i < source.length - 1 && !(source[i] === "*" && source[i + 1] === "/")) {
|
|
28
|
+
if (source[i] === "\n") out += "\n";
|
|
29
|
+
i++;
|
|
30
|
+
}
|
|
31
|
+
i += 2;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
35
|
+
const quote = c;
|
|
36
|
+
out += c;
|
|
37
|
+
i++;
|
|
38
|
+
while (i < source.length && source[i] !== quote) {
|
|
39
|
+
if (source[i] === "\\" && i + 1 < source.length) {
|
|
40
|
+
out += source[i] + source[i + 1];
|
|
41
|
+
i += 2;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
out += source[i];
|
|
45
|
+
i++;
|
|
46
|
+
}
|
|
47
|
+
if (i < source.length) {
|
|
48
|
+
out += source[i];
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
out += c;
|
|
54
|
+
i++;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const CHAIN =
|
|
60
|
+
/(?:GetService\s*<\s*([A-Za-z_]\w*)\s*>\s*\(\s*\)|GetService\s*\(\s*"([A-Za-z_]\w*)"\s*\)|(?:\bgame\s*\.\s*)?([A-Za-z_]\w*))((?:\s*\.\s*(?:WaitForChild|FindFirstChild|FindFirstChildOfClass|FindFirstChildWhichIsA)\s*\(\s*"[^"]+"\s*(?:,[^)]*)?\))+)/g;
|
|
61
|
+
|
|
62
|
+
const CHILD_NAME = /\.\s*(?:WaitForChild|FindFirstChild|FindFirstChildOfClass|FindFirstChildWhichIsA)\s*\(\s*"([^"]+)"/g;
|
|
63
|
+
|
|
64
|
+
const BIND =
|
|
65
|
+
/(?:auto|Instance|[A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*=\s*(?:GetService\s*<\s*([A-Za-z_]\w*)\s*>\s*\(\s*\)|GetService\s*\(\s*"([A-Za-z_]\w*)"\s*\)|([A-Za-z_]\w*))((?:\s*\.\s*(?:WaitForChild|FindFirstChild|FindFirstChildOfClass|FindFirstChildWhichIsA)\s*\(\s*"[^"]+"\s*(?:,[^)]*)?\))*)/g;
|
|
66
|
+
|
|
67
|
+
function lineCol(source: string, index: number): { line: number; column: number } {
|
|
68
|
+
let line = 1;
|
|
69
|
+
let column = 1;
|
|
70
|
+
for (let i = 0; i < index && i < source.length; i++) {
|
|
71
|
+
if (source[i] === "\n") {
|
|
72
|
+
line++;
|
|
73
|
+
column = 1;
|
|
74
|
+
} else {
|
|
75
|
+
column++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { line, column };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function serviceRoot(profile: DatamodelProfile, serviceName: string): DatamodelNode | null {
|
|
82
|
+
return lookupChild(profile.root, serviceName) || null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function extractChildNames(chainTail: string): string[] {
|
|
86
|
+
const names: string[] = [];
|
|
87
|
+
CHILD_NAME.lastIndex = 0;
|
|
88
|
+
let m: RegExpExecArray | null;
|
|
89
|
+
while ((m = CHILD_NAME.exec(chainTail))) {
|
|
90
|
+
names.push(m[1]);
|
|
91
|
+
}
|
|
92
|
+
return names;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function walkFrom(profile: DatamodelProfile, rootName: string, names: string[]): { node: DatamodelNode; walked: string; missing?: string } | null {
|
|
96
|
+
let node = serviceRoot(profile, rootName) || profile.byPath[rootName] || null;
|
|
97
|
+
if (!node) return null;
|
|
98
|
+
let walked = rootName;
|
|
99
|
+
for (const childName of names) {
|
|
100
|
+
const child = lookupChild(node, childName);
|
|
101
|
+
if (!child) {
|
|
102
|
+
return { node, walked, missing: childName };
|
|
103
|
+
}
|
|
104
|
+
node = child;
|
|
105
|
+
walked = `${walked}.${childName}`;
|
|
106
|
+
}
|
|
107
|
+
return { node, walked };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildVarEnv(clean: string, profile: DatamodelProfile): Map<string, DatamodelNode> {
|
|
111
|
+
const env = new Map<string, DatamodelNode>();
|
|
112
|
+
BIND.lastIndex = 0;
|
|
113
|
+
let m: RegExpExecArray | null;
|
|
114
|
+
while ((m = BIND.exec(clean))) {
|
|
115
|
+
const varName = m[1];
|
|
116
|
+
const rootName = m[2] || m[3] || m[4];
|
|
117
|
+
const chainTail = m[5] || "";
|
|
118
|
+
if (!rootName) continue;
|
|
119
|
+
const names = extractChildNames(chainTail);
|
|
120
|
+
const walked = walkFrom(profile, rootName, names);
|
|
121
|
+
if (walked && !walked.missing) {
|
|
122
|
+
env.set(varName, walked.node);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return env;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validateChain(
|
|
129
|
+
profile: DatamodelProfile,
|
|
130
|
+
rootName: string,
|
|
131
|
+
names: string[],
|
|
132
|
+
source: string,
|
|
133
|
+
index: number,
|
|
134
|
+
fileHint?: string,
|
|
135
|
+
env?: Map<string, DatamodelNode>,
|
|
136
|
+
): DatamodelViolation[] {
|
|
137
|
+
const out: DatamodelViolation[] = [];
|
|
138
|
+
let node = serviceRoot(profile, rootName) || profile.byPath[rootName] || env?.get(rootName) || null;
|
|
139
|
+
if (!node) {
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
let walked = node.path;
|
|
143
|
+
for (const childName of names) {
|
|
144
|
+
const child = lookupChild(node, childName);
|
|
145
|
+
if (!child) {
|
|
146
|
+
const { line, column } = lineCol(source, index);
|
|
147
|
+
const known = Object.keys(node.children).slice(0, 12).join(", ") || "(none)";
|
|
148
|
+
out.push({
|
|
149
|
+
code: "CLUAU_DM_MISSING_CHILD",
|
|
150
|
+
message: `Typed DataModel: "${childName}" is not a child of ${walked} (${node.className}). Known: ${known}`,
|
|
151
|
+
line,
|
|
152
|
+
column,
|
|
153
|
+
severity: "error",
|
|
154
|
+
pathHint: `${node.path}.${childName}`,
|
|
155
|
+
});
|
|
156
|
+
void fileHint;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
node = child;
|
|
160
|
+
walked = node.path;
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function checkDatamodelPaths(source: string, profile: DatamodelProfile): DatamodelViolation[] {
|
|
166
|
+
const clean = stripComments(source);
|
|
167
|
+
const env = buildVarEnv(clean, profile);
|
|
168
|
+
const violations: DatamodelViolation[] = [];
|
|
169
|
+
CHAIN.lastIndex = 0;
|
|
170
|
+
let m: RegExpExecArray | null;
|
|
171
|
+
while ((m = CHAIN.exec(clean))) {
|
|
172
|
+
const rootName = m[1] || m[2] || m[3];
|
|
173
|
+
const names = extractChildNames(m[4]);
|
|
174
|
+
if (!rootName || !names.length) continue;
|
|
175
|
+
violations.push(...validateChain(profile, rootName, names, source, m.index, undefined, env));
|
|
176
|
+
}
|
|
177
|
+
return violations;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function assertDatamodelSafe(source: string, profile: DatamodelProfile): void {
|
|
181
|
+
const errors = checkDatamodelPaths(source, profile).filter((v) => v.severity === "error");
|
|
182
|
+
if (!errors.length) return;
|
|
183
|
+
throw new Error(`Typed DataModel:\n${errors.map((e) => ` ${e.line}:${e.column} ${e.code} ${e.message}`).join("\n")}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function formatDatamodelViolations(file: string, violations: DatamodelViolation[]): string {
|
|
187
|
+
return violations.map((v) => `${file}:${v.line}:${v.column}: ${v.severity}: ${v.code} ${v.message}`).join("\n");
|
|
188
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit Typed DataModel profile JSON + CL++ IntelliSense header.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import type { DatamodelNode, DatamodelProfile } from "./datamodel.js";
|
|
8
|
+
import { buildDatamodelProfile } from "./datamodel.js";
|
|
9
|
+
|
|
10
|
+
function safeIdent(name: string): string {
|
|
11
|
+
const cleaned = name.replace(/[^A-Za-z0-9_]/g, "_");
|
|
12
|
+
return /^[A-Za-z_]/.test(cleaned) ? cleaned : `_${cleaned}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function emitStruct(node: DatamodelNode, structName: string, depth: number): string {
|
|
16
|
+
const pad = "\t".repeat(depth);
|
|
17
|
+
const childEntries = Object.values(node.children);
|
|
18
|
+
if (!childEntries.length) {
|
|
19
|
+
return `${pad}// ${node.path} : ${node.className}\n`;
|
|
20
|
+
}
|
|
21
|
+
const lines: string[] = [];
|
|
22
|
+
lines.push(`${pad}struct ${structName} {`);
|
|
23
|
+
for (const child of childEntries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
24
|
+
const field = safeIdent(child.name);
|
|
25
|
+
const nested = `${structName}_${field}`;
|
|
26
|
+
if (Object.keys(child.children).length) {
|
|
27
|
+
lines.push(emitStruct(child, nested, depth + 1).trimEnd());
|
|
28
|
+
lines.push(`${pad}\t${nested} ${field}; // ${child.className}`);
|
|
29
|
+
} else {
|
|
30
|
+
lines.push(`${pad}\t${child.className} ${field};`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
lines.push(`${pad}};`);
|
|
34
|
+
return lines.join("\n") + "\n";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function generateDatamodelHeader(profile: DatamodelProfile): string {
|
|
38
|
+
const body = emitStruct(profile.root, "Dm_DataModel", 0);
|
|
39
|
+
return [
|
|
40
|
+
"#pragma once",
|
|
41
|
+
"// Generated by Cluaupp Typed DataModel — do not edit.",
|
|
42
|
+
`// Project: ${profile.projectName} (${profile.projectJson})`,
|
|
43
|
+
`// schemaVersion: ${profile.schemaVersion}`,
|
|
44
|
+
"// IntelliSense / static check aid. Runtime remains Roblox FindFirstChild / WaitForChild.",
|
|
45
|
+
"",
|
|
46
|
+
body,
|
|
47
|
+
"// Root mirror (optional binding for IntelliSense only):",
|
|
48
|
+
"struct Dm_DataModel Dm;",
|
|
49
|
+
"",
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function profileWithoutIndex(profile: DatamodelProfile): Omit<DatamodelProfile, "byPath"> & { paths: string[] } {
|
|
54
|
+
const { byPath, ...rest } = profile;
|
|
55
|
+
return { ...rest, paths: Object.keys(byPath).filter((p) => p !== "DataModel").sort() };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function writeDatamodelArtifacts(
|
|
59
|
+
projectRoot: string,
|
|
60
|
+
opts?: { projectJson?: string; outDir?: string; headerPath?: string },
|
|
61
|
+
): { profile: DatamodelProfile; profilePath: string; headerPath: string } | null {
|
|
62
|
+
const profile = buildDatamodelProfile(projectRoot, opts?.projectJson);
|
|
63
|
+
if (!profile) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const outDir = opts?.outDir || path.join(projectRoot, ".cluaupp");
|
|
67
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
68
|
+
const profilePath = path.join(outDir, "datamodel-profile.json");
|
|
69
|
+
fs.writeFileSync(profilePath, JSON.stringify(profileWithoutIndex(profile), null, "\t") + "\n", "utf8");
|
|
70
|
+
|
|
71
|
+
const headerPath =
|
|
72
|
+
opts?.headerPath ||
|
|
73
|
+
path.join(projectRoot, "include", "clpp", "generated", "datamodel.clh");
|
|
74
|
+
fs.mkdirSync(path.dirname(headerPath), { recursive: true });
|
|
75
|
+
fs.writeFileSync(headerPath, generateDatamodelHeader(profile), "utf8");
|
|
76
|
+
|
|
77
|
+
return { profile, profilePath, headerPath };
|
|
78
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed DataModel v1 — Rojo default.project.json → instance graph.
|
|
3
|
+
* Used for path validation at compile time and generated IntelliSense headers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { parse } from "comment-json";
|
|
9
|
+
|
|
10
|
+
export const DATAMODEL_SCHEMA_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
export interface DatamodelNode {
|
|
13
|
+
/** Roblox instance name (tree key). */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Dot path from game, e.g. ReplicatedStorage.Shared (DataModel root is "DataModel"). */
|
|
16
|
+
path: string;
|
|
17
|
+
className: string;
|
|
18
|
+
/** Project-relative fs path when backed by $path. */
|
|
19
|
+
fsPath?: string;
|
|
20
|
+
children: Record<string, DatamodelNode>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DatamodelProfile {
|
|
24
|
+
schemaVersion: number;
|
|
25
|
+
projectName: string;
|
|
26
|
+
projectJson: string;
|
|
27
|
+
root: DatamodelNode;
|
|
28
|
+
/** Flat index path → node */
|
|
29
|
+
byPath: Record<string, DatamodelNode>;
|
|
30
|
+
generatedAt: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
34
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return value as Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function posix(p: string): string {
|
|
41
|
+
return p.replace(/\\/g, "/");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function childPath(parentPath: string, name: string): string {
|
|
45
|
+
if (!parentPath || parentPath === "DataModel") {
|
|
46
|
+
return name;
|
|
47
|
+
}
|
|
48
|
+
return `${parentPath}.${name}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readMetaClassName(dir: string): string | undefined {
|
|
52
|
+
for (const metaName of ["init.meta.json", "init.meta.jsonc"]) {
|
|
53
|
+
const file = path.join(dir, metaName);
|
|
54
|
+
if (!fs.existsSync(file)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const meta = asRecord(parse(fs.readFileSync(file, "utf8")));
|
|
59
|
+
if (meta && typeof meta.className === "string") {
|
|
60
|
+
return meta.className;
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
/* ignore */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function classFromFileName(fileName: string): { name: string; className: string } | null {
|
|
70
|
+
const lower = fileName.toLowerCase();
|
|
71
|
+
if (lower === "init.meta.json" || lower === "init.meta.jsonc") {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (/\.server\.(luau|lua|clpp)$/i.test(fileName)) {
|
|
75
|
+
return { name: fileName.replace(/\.server\.(luau|lua|clpp)$/i, ""), className: "Script" };
|
|
76
|
+
}
|
|
77
|
+
if (/\.client\.(luau|lua|clpp)$/i.test(fileName)) {
|
|
78
|
+
return { name: fileName.replace(/\.client\.(luau|lua|clpp)$/i, ""), className: "LocalScript" };
|
|
79
|
+
}
|
|
80
|
+
if (/\.plugin\.(luau|lua|clpp)$/i.test(fileName)) {
|
|
81
|
+
return { name: fileName.replace(/\.plugin\.(luau|lua|clpp)$/i, ""), className: "Script" };
|
|
82
|
+
}
|
|
83
|
+
if (/^init\.(luau|lua|clpp|clp)$/i.test(fileName)) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (/\.(luau|lua|clpp|clp|clh)$/i.test(fileName)) {
|
|
87
|
+
return {
|
|
88
|
+
name: fileName.replace(/\.(luau|lua|clpp|clp|clh)$/i, ""),
|
|
89
|
+
className: "ModuleScript",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function scanFsChildren(absDir: string, parentPath: string, projectRoot: string): Record<string, DatamodelNode> {
|
|
96
|
+
const children: Record<string, DatamodelNode> = {};
|
|
97
|
+
if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) {
|
|
98
|
+
return children;
|
|
99
|
+
}
|
|
100
|
+
let entries: fs.Dirent[];
|
|
101
|
+
try {
|
|
102
|
+
entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
103
|
+
} catch {
|
|
104
|
+
return children;
|
|
105
|
+
}
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (entry.name.startsWith(".")) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const full = path.join(absDir, entry.name);
|
|
111
|
+
if (entry.isDirectory()) {
|
|
112
|
+
const className = readMetaClassName(full) || "Folder";
|
|
113
|
+
const nodePath = childPath(parentPath, entry.name);
|
|
114
|
+
children[entry.name] = {
|
|
115
|
+
name: entry.name,
|
|
116
|
+
path: nodePath,
|
|
117
|
+
className,
|
|
118
|
+
fsPath: posix(path.relative(projectRoot, full) || "."),
|
|
119
|
+
children: scanFsChildren(full, nodePath, projectRoot),
|
|
120
|
+
};
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!entry.isFile()) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const mapped = classFromFileName(entry.name);
|
|
127
|
+
if (!mapped || !mapped.name) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const nodePath = childPath(parentPath, mapped.name);
|
|
131
|
+
children[mapped.name] = {
|
|
132
|
+
name: mapped.name,
|
|
133
|
+
path: nodePath,
|
|
134
|
+
className: mapped.className,
|
|
135
|
+
fsPath: posix(path.relative(projectRoot, full)),
|
|
136
|
+
children: {},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return children;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function walkNamedNode(node: unknown, name: string, parentPath: string, projectRoot: string): DatamodelNode {
|
|
143
|
+
const record = asRecord(node) || {};
|
|
144
|
+
const nodePath = childPath(parentPath, name);
|
|
145
|
+
const className = typeof record.$className === "string" ? record.$className : "Folder";
|
|
146
|
+
const fsRel = typeof record.$path === "string" ? record.$path : undefined;
|
|
147
|
+
const absFs = fsRel ? path.resolve(projectRoot, fsRel) : undefined;
|
|
148
|
+
|
|
149
|
+
const children: Record<string, DatamodelNode> = {};
|
|
150
|
+
for (const [key, child] of Object.entries(record)) {
|
|
151
|
+
if (key.startsWith("$")) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
children[key] = walkNamedNode(child, key, nodePath, projectRoot);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (absFs && fs.existsSync(absFs) && fs.statSync(absFs).isDirectory()) {
|
|
158
|
+
const scanned = scanFsChildren(absFs, nodePath, projectRoot);
|
|
159
|
+
for (const [key, child] of Object.entries(scanned)) {
|
|
160
|
+
if (!children[key]) {
|
|
161
|
+
children[key] = child;
|
|
162
|
+
} else {
|
|
163
|
+
children[key] = {
|
|
164
|
+
...children[key],
|
|
165
|
+
fsPath: children[key].fsPath || child.fsPath,
|
|
166
|
+
children: { ...child.children, ...children[key].children },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
name,
|
|
174
|
+
path: nodePath,
|
|
175
|
+
className,
|
|
176
|
+
fsPath: fsRel ? posix(fsRel) : undefined,
|
|
177
|
+
children,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function indexByPath(node: DatamodelNode, into: Record<string, DatamodelNode>): void {
|
|
182
|
+
into[node.path] = node;
|
|
183
|
+
for (const child of Object.values(node.children)) {
|
|
184
|
+
indexByPath(child, into);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function findProjectJson(projectRoot: string, explicit?: string): string | null {
|
|
189
|
+
const candidates = [
|
|
190
|
+
explicit,
|
|
191
|
+
path.join(projectRoot, "default.project.json"),
|
|
192
|
+
path.join(projectRoot, "default.project.jsonc"),
|
|
193
|
+
].filter(Boolean) as string[];
|
|
194
|
+
for (const c of candidates) {
|
|
195
|
+
const abs = path.resolve(c);
|
|
196
|
+
if (fs.existsSync(abs)) {
|
|
197
|
+
return abs;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function buildDatamodelProfile(projectRoot: string, projectJsonPath?: string): DatamodelProfile | null {
|
|
204
|
+
const jsonPath = findProjectJson(projectRoot, projectJsonPath);
|
|
205
|
+
if (!jsonPath) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const raw = parse(fs.readFileSync(jsonPath, "utf8")) as { name?: string; tree?: unknown };
|
|
209
|
+
const tree = asRecord(raw.tree) || {};
|
|
210
|
+
const className = typeof tree.$className === "string" ? tree.$className : "DataModel";
|
|
211
|
+
const children: Record<string, DatamodelNode> = {};
|
|
212
|
+
for (const [key, child] of Object.entries(tree)) {
|
|
213
|
+
if (key.startsWith("$")) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
children[key] = walkNamedNode(child, key, "DataModel", projectRoot);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const root: DatamodelNode = {
|
|
220
|
+
name: "DataModel",
|
|
221
|
+
path: "DataModel",
|
|
222
|
+
className,
|
|
223
|
+
children,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const byPath: Record<string, DatamodelNode> = {};
|
|
227
|
+
indexByPath(root, byPath);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
schemaVersion: DATAMODEL_SCHEMA_VERSION,
|
|
231
|
+
projectName: typeof raw.name === "string" ? raw.name : "game",
|
|
232
|
+
projectJson: posix(path.relative(projectRoot, jsonPath) || path.basename(jsonPath)),
|
|
233
|
+
root,
|
|
234
|
+
byPath,
|
|
235
|
+
generatedAt: new Date().toISOString(),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function lookupChild(parent: DatamodelNode, childName: string): DatamodelNode | undefined {
|
|
240
|
+
return parent.children[childName];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function resolvePath(profile: DatamodelProfile, segments: string[]): DatamodelNode | null {
|
|
244
|
+
let node: DatamodelNode = profile.root;
|
|
245
|
+
for (const seg of segments) {
|
|
246
|
+
if (seg === "game" || seg === "DataModel") {
|
|
247
|
+
node = profile.root;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const next = lookupChild(node, seg);
|
|
251
|
+
if (!next) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
node = next;
|
|
255
|
+
}
|
|
256
|
+
return node;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function listChildNames(node: DatamodelNode): string[] {
|
|
260
|
+
return Object.keys(node.children).sort();
|
|
261
|
+
}
|