@gi-tcg/gts-transpiler 0.1.1 → 0.3.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.
package/package.json CHANGED
@@ -1,32 +1,31 @@
1
1
  {
2
2
  "name": "@gi-tcg/gts-transpiler",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
+ "repository": "https://github.com/piovium/gts.git",
5
+ "license": "Apache-2.0",
4
6
  "type": "module",
5
7
  "exports": {
6
8
  ".": {
7
- "types": "./src/index.ts",
9
+ "types": "./dist/index.d.ts",
8
10
  "bun": "./src/index.ts",
9
- "import": "./dist/index.js"
11
+ "default": "./dist/index.js"
10
12
  }
11
13
  },
12
14
  "files": [
13
15
  "src",
14
16
  "dist"
15
17
  ],
16
- "scripts": {
17
- "build": "bun build --outdir=dist --target=node --format=esm --conditions='bun' --packages=external src/index.ts",
18
- "prepublishOnly": "bun run build"
19
- },
18
+ "scripts": {},
20
19
  "dependencies": {
21
20
  "@jridgewell/sourcemap-codec": "^1.5.5",
22
21
  "@sveltejs/acorn-typescript": "^1.0.8",
23
- "acorn": "^8.15.0",
24
- "esrap": "^2.2.1",
22
+ "acorn": "8.15.0",
23
+ "esrap": "2.2.1",
25
24
  "magic-string": "^0.30.21",
26
25
  "zimmerframe": "^1.1.4"
27
26
  },
28
27
  "devDependencies": {
29
- "@types/bun": "^1.3.4",
28
+ "@types/node": "^24.3.0",
30
29
  "@types/estree": "^1.0.8",
31
30
  "@volar/language-core": "^2.4.27"
32
31
  }
package/src/config.ts ADDED
@@ -0,0 +1,221 @@
1
+ import type { TranspileOption } from "./transform/gts";
2
+
3
+ export interface PathPolyfill {
4
+ isAbsolute(filePath: string): boolean;
5
+ dirname(filePath: string): string;
6
+ resolve(...segments: string[]): string;
7
+ }
8
+
9
+ export const path: PathPolyfill = globalThis.require
10
+ ? (globalThis.require("node:path") as typeof import("node:path"))
11
+ : {
12
+ isAbsolute(filePath) {
13
+ return filePath.startsWith("/");
14
+ },
15
+ dirname(filePath) {
16
+ if (!filePath) {
17
+ return ".";
18
+ }
19
+ const normalized = normalizeSlashes(filePath);
20
+ if (normalized === "/") {
21
+ return "/";
22
+ }
23
+ const trimmed =
24
+ normalized.length > 1 && normalized.endsWith("/")
25
+ ? normalized.slice(0, -1)
26
+ : normalized;
27
+ const idx = trimmed.lastIndexOf("/");
28
+ if (idx < 0) {
29
+ return ".";
30
+ }
31
+ if (idx === 0) {
32
+ return "/";
33
+ }
34
+ return trimmed.slice(0, idx);
35
+ },
36
+ resolve(...segments) {
37
+ if (segments.length === 0) {
38
+ return ".";
39
+ }
40
+ let resolved = "";
41
+ for (let i = segments.length - 1; i >= 0; i--) {
42
+ const segment = segments[i];
43
+ if (!segment) {
44
+ continue;
45
+ }
46
+ if (resolved) {
47
+ resolved = `${segment}/${resolved}`;
48
+ } else {
49
+ resolved = segment;
50
+ }
51
+ if (path.isAbsolute(segment)) {
52
+ break;
53
+ }
54
+ }
55
+ return normalizeResolvedPath(resolved || ".");
56
+ },
57
+ };
58
+
59
+ function normalizeSlashes(filePath: string): string {
60
+ return filePath.replace(/\\+/g, "/");
61
+ }
62
+
63
+ function normalizeResolvedPath(filePath: string): string {
64
+ const normalized = normalizeSlashes(filePath);
65
+ const isAbsolute = normalized.startsWith("/");
66
+ const parts = normalized.split("/");
67
+ const stack: string[] = [];
68
+ for (const part of parts) {
69
+ if (!part || part === ".") {
70
+ continue;
71
+ }
72
+ if (part === "..") {
73
+ if (stack.length > 0 && stack[stack.length - 1] !== "..") {
74
+ stack.pop();
75
+ } else if (!isAbsolute) {
76
+ stack.push("..");
77
+ }
78
+ continue;
79
+ }
80
+ stack.push(part);
81
+ }
82
+ if (isAbsolute) {
83
+ return `/${stack.join("/")}` || "/";
84
+ }
85
+ return stack.join("/") || ".";
86
+ }
87
+
88
+ export interface GtsConfig extends TranspileOption {}
89
+
90
+ export interface PackageJson {
91
+ gamingTs?: GtsConfig;
92
+ }
93
+
94
+ type ReadFileFn = (path: string, encoding: "utf8") => string;
95
+ type ReadFileAsyncFn = (path: string, encoding: "utf8") => Promise<string>;
96
+
97
+ export interface ResolveGtsConfigSyncOptions {
98
+ readFileFn: ReadFileFn;
99
+ cwd?: string;
100
+ stopDir?: string;
101
+ }
102
+
103
+ export interface ResolveGtsConfigAsyncOptions {
104
+ readFileFn: ReadFileAsyncFn;
105
+ cwd?: string;
106
+ stopDir?: string;
107
+ }
108
+
109
+ const DEFAULT_GTS_CONFIG: Required<GtsConfig> = {
110
+ runtimeImportSource: "@gi-tcg/gts-runtime",
111
+ providerImportSource: "@gi-tcg/core/gts",
112
+ shortcutFunctionPreludes: [
113
+ "cryo",
114
+ "hydro",
115
+ "pyro",
116
+ "electro",
117
+ "anemo",
118
+ "geo",
119
+ "dendro",
120
+ "omni",
121
+ ],
122
+ queryBindings: ["my", "opp"],
123
+ };
124
+
125
+ function* resolveGtsConfigImpl(
126
+ filePath: string,
127
+ inlineConfig: GtsConfig = {},
128
+ options: ResolveGtsConfigAsyncOptions | ResolveGtsConfigSyncOptions,
129
+ ): Generator<string | Promise<string>, Required<GtsConfig>, string> {
130
+ const startDir = normalizeStartDir(filePath, options.cwd);
131
+ const stopDir = options.stopDir ? path.resolve(options.stopDir) : void 0;
132
+ const pkgConfig = yield* findNearestPackageConfig(
133
+ options.readFileFn,
134
+ startDir,
135
+ stopDir,
136
+ );
137
+ return {
138
+ ...DEFAULT_GTS_CONFIG,
139
+ ...pkgConfig,
140
+ ...inlineConfig,
141
+ };
142
+ }
143
+
144
+ export async function resolveGtsConfig(
145
+ filePath: string,
146
+ inlineConfig: GtsConfig,
147
+ options: ResolveGtsConfigAsyncOptions,
148
+ ): Promise<Required<GtsConfig>> {
149
+ const generator = resolveGtsConfigImpl(filePath, inlineConfig, options);
150
+ let result = generator.next();
151
+ while (!result.done) {
152
+ const toRead = result.value;
153
+ const content = await toRead;
154
+ result = generator.next(content);
155
+ }
156
+ return result.value;
157
+ }
158
+
159
+ export function resolveGtsConfigSync(
160
+ filePath: string,
161
+ inlineConfig: GtsConfig,
162
+ options: ResolveGtsConfigSyncOptions,
163
+ ): Required<GtsConfig> {
164
+ const generator = resolveGtsConfigImpl(filePath, inlineConfig, options);
165
+ let result = generator.next();
166
+ while (!result.done) {
167
+ const toRead = result.value;
168
+ if (toRead instanceof Promise) {
169
+ throw new Error(
170
+ "resolveGtsConfigSync received a Promise. Did you mean to use resolveGtsConfig instead?",
171
+ );
172
+ }
173
+ const content = toRead;
174
+ result = generator.next(content);
175
+ }
176
+ return result.value;
177
+ }
178
+
179
+ function normalizeStartDir(sourceFile: string, cwd?: string): string {
180
+ const absolute = path.isAbsolute(sourceFile)
181
+ ? sourceFile
182
+ : path.resolve(cwd || ".", sourceFile);
183
+ return path.dirname(absolute);
184
+ }
185
+
186
+ function* findNearestPackageConfig(
187
+ readFileFn: ReadFileFn | ReadFileAsyncFn,
188
+ startDir: string,
189
+ stopDir?: string,
190
+ ): Generator<string | Promise<string>, GtsConfig, string> {
191
+ let currentDir = startDir;
192
+ while (true) {
193
+ const pkgPath = path.resolve(currentDir, "package.json");
194
+ const config = yield* readPackageConfig(readFileFn, pkgPath);
195
+ if (config) {
196
+ return config;
197
+ }
198
+ const parentDir = path.dirname(currentDir);
199
+ if (parentDir === currentDir) {
200
+ break;
201
+ }
202
+ if (stopDir && currentDir === stopDir) {
203
+ break;
204
+ }
205
+ currentDir = parentDir;
206
+ }
207
+ return {};
208
+ }
209
+
210
+ function* readPackageConfig(
211
+ readFileFn: ReadFileFn | ReadFileAsyncFn,
212
+ pkgPath: string,
213
+ ): Generator<string | Promise<string>, GtsConfig | undefined, string> {
214
+ try {
215
+ const content = yield readFileFn(pkgPath, "utf8");
216
+ const parsed = JSON.parse(content) as PackageJson;
217
+ return parsed.gamingTs;
218
+ } catch {
219
+ return;
220
+ }
221
+ }
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ export { GtsTranspilerError } from "./error";
11
11
  export function transpile(
12
12
  source: string,
13
13
  filename: string,
14
- option: TranspileOption
14
+ option: TranspileOption,
15
15
  ): TranspileResult {
16
16
  const ast = parse(source);
17
17
  return transform(ast, option, {
@@ -23,9 +23,11 @@ export function transpile(
23
23
  export function transpileForVolar(
24
24
  source: string,
25
25
  filename: string,
26
- option: TranspileOption
26
+ option: TranspileOption,
27
27
  ): VolarMappingResult {
28
- const ast = parseLoose(source);
28
+ const ast = parseLoose(source, {
29
+ recordCallLParens: true,
30
+ });
29
31
  return transformForVolar(ast, option, {
30
32
  content: source,
31
33
  filename,
@@ -33,3 +35,10 @@ export function transpileForVolar(
33
35
  }
34
36
 
35
37
  export type { TranspileOption, TranspileResult, VolarMappingResult };
38
+ export {
39
+ resolveGtsConfig,
40
+ resolveGtsConfigSync,
41
+ type GtsConfig,
42
+ path,
43
+ type PathPolyfill,
44
+ } from "./config";
package/src/keywords.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const specialIdentifiers = [
1
+ export const specialIdentifiers: string[] = [
2
2
  // Always Reserved Words
3
3
  "break",
4
4
  "case",
@@ -0,0 +1,10 @@
1
+ import { Comment, Node } from "estree";
2
+ import { Options } from "acorn";
3
+
4
+ export function getCommentHandlers(
5
+ source: string,
6
+ comments: Comment[],
7
+ ): {
8
+ onComment: Options["onComment"];
9
+ addComments: (ast: Node) => void;
10
+ };
@@ -75,7 +75,7 @@ export interface GtsPluginOption {
75
75
 
76
76
  export function gtsPlugin(options: GtsPluginOption = {}) {
77
77
  return function gtsPluginTransformer(
78
- Parser: typeof ParserClass
78
+ Parser: typeof ParserClass,
79
79
  ): typeof ParserClass {
80
80
  const skipWhiteSpace = /(?:\s|\/\/.*|\/\*[^]*?\*\/)*/g;
81
81
  const lineBreak = /\r\n?|\n|\u2028|\u2029/;
@@ -95,7 +95,7 @@ export function gtsPlugin(options: GtsPluginOption = {}) {
95
95
  override parseStatement(
96
96
  context?: string | null,
97
97
  topLevel?: boolean,
98
- exports?: AST.ExportSpecifier
98
+ exports?: AST.ExportSpecifier,
99
99
  ) {
100
100
  if (topLevel && this.gts_isDefineStatement()) {
101
101
  const node = this.startNode() as AST.GTSDefineStatement;
@@ -119,13 +119,20 @@ export function gtsPlugin(options: GtsPluginOption = {}) {
119
119
  gts_parseNamedAttributeDefinition() {
120
120
  const node = this.startNode() as AST.GTSNamedAttributeDefinition;
121
121
  // AttributeName
122
- let name: any;
122
+ const start = this.start;
123
+ let name: AST.Identifier | AST.SimpleLiteral;
123
124
  if (this.type.label === "string") {
124
- name = this.parseExprAtom();
125
+ name = this.parseExprAtom() as AST.SimpleLiteral;
126
+ if (typeof name.value === "string" && name.value.startsWith("~")) {
127
+ this.raise(
128
+ start,
129
+ `Attribute name starts with '~' is reserved for internal use.`,
130
+ );
131
+ }
125
132
  } else if (this.type.label === "name") {
126
133
  name = this.parseIdent();
127
134
  } else {
128
- this.raise(this.start, "Expected attribute name");
135
+ this.raise(start, "Expected attribute name");
129
136
  }
130
137
  node.name = name;
131
138
  // AttributeBody
@@ -215,7 +222,7 @@ export function gtsPlugin(options: GtsPluginOption = {}) {
215
222
  if (stmt.type === "GTSDefineStatement") {
216
223
  this.raise(
217
224
  startPos,
218
- "DefineStatement is not allowed in direct function."
225
+ "DefineStatement is not allowed in direct function.",
219
226
  );
220
227
  }
221
228
  node.body.push(stmt);
@@ -274,13 +281,13 @@ export function gtsPlugin(options: GtsPluginOption = {}) {
274
281
  override parseExprAtom(
275
282
  refDestructuringErrors?: Parse.DestructuringErrors,
276
283
  forInit?: boolean | "await",
277
- forNew?: boolean
284
+ forNew?: boolean,
278
285
  ): AST.Expression {
279
286
  if (this.type === tokTypes.colon) {
280
287
  if (!this.isShortcutContext) {
281
288
  this.raise(
282
289
  this.start,
283
- "ShortcutArgumentExpression ':' must be inside ShortcutFunction or DirectShortcutFunction."
290
+ "ShortcutArgumentExpression ':' must be inside ShortcutFunction or DirectShortcutFunction.",
284
291
  );
285
292
  }
286
293
  const node = this.startNode() as AST.GTSShortcutArgumentExpression;
@@ -307,7 +314,7 @@ export function gtsPlugin(options: GtsPluginOption = {}) {
307
314
  refDestructuringErrors?: Parse.DestructuringErrors | null,
308
315
  sawUnary?: boolean,
309
316
  incDec?: boolean,
310
- forInit?: boolean | "await"
317
+ forInit?: boolean | "await",
311
318
  ): AST.Expression {
312
319
  if (this.isContextual("query")) {
313
320
  const expr = this.gts_parseQueryExpression();
@@ -320,12 +327,12 @@ export function gtsPlugin(options: GtsPluginOption = {}) {
320
327
  refDestructuringErrors,
321
328
  sawUnary,
322
329
  incDec,
323
- forInit
330
+ forInit,
324
331
  );
325
332
  }
326
333
 
327
334
  gts_parseQueryExpression(
328
- forInit?: boolean | "await"
335
+ forInit?: boolean | "await",
329
336
  ): AST.GTSQueryExpression {
330
337
  const node = this.startNode() as AST.GTSQueryExpression;
331
338
  this.next(); // consume 'query'
@@ -1,10 +1,11 @@
1
1
  import { Parser } from "acorn";
2
- import type { Program } from "estree";
2
+ import type { Position, Program } from "estree";
3
3
  import { tsPlugin } from "@sveltejs/acorn-typescript";
4
4
  import { gtsPlugin, type GtsPluginOption } from "./gts_plugin.js";
5
5
  import { loosePlugin } from "./loose_plugin.js";
6
6
  import { getCommentHandlers } from "./comment.js";
7
7
  import { GtsTranspilerError } from "../error.js";
8
+ import { recordCallLParenPlugin } from "./record_call_lparen_plugin.js";
8
9
 
9
10
  const TsParser = Parser.extend(tsPlugin());
10
11
 
@@ -29,15 +30,24 @@ export function parse(input: string, options?: GtsPluginOption): Program {
29
30
  }
30
31
  }
31
32
 
32
- export function parseLoose(input: string, options?: GtsPluginOption): Program {
33
+ export interface ParseLooseOptions extends GtsPluginOption {
34
+ recordCallLParens?: boolean;
35
+ }
36
+
37
+ export function parseLoose(
38
+ input: string,
39
+ options?: ParseLooseOptions,
40
+ ): Program {
33
41
  try {
34
42
  const GtsParser = TsParser.extend(
35
43
  loosePlugin(),
44
+ ...(options?.recordCallLParens ? [recordCallLParenPlugin()] : []),
36
45
  gtsPlugin({
37
- ...options,
38
- allowEmptyShortcutMember: true,
39
- allowEmptyPositionalAttribute: true,
40
- })
46
+ allowEmptyShortcutMember:
47
+ options?.allowEmptyPositionalAttribute || true,
48
+ allowEmptyPositionalAttribute:
49
+ options?.allowEmptyPositionalAttribute || true,
50
+ }),
41
51
  );
42
52
  const { onComment, addComments } = getCommentHandlers(input, []);
43
53
  const ast = GtsParser.parse(input, {
@@ -15,7 +15,7 @@ export function loosePlugin() {
15
15
  return super.parseIdent(liberal);
16
16
  }
17
17
  };
18
- private _proxiedThis = new Proxy(this, {
18
+ readonly #proxiedThis = new Proxy(this, {
19
19
  get: (target, prop) => {
20
20
  if (prop === "parseIdent") {
21
21
  return this._patchedParseIdent;
@@ -45,7 +45,7 @@ export function loosePlugin() {
45
45
  forInit?: boolean | "await",
46
46
  ): AST.Expression {
47
47
  return super.parseSubscript.call(
48
- this._proxiedThis,
48
+ this.#proxiedThis,
49
49
  base,
50
50
  startPos,
51
51
  startLoc,
@@ -0,0 +1,85 @@
1
+ import { tokTypes, type Parser } from "acorn";
2
+ import type { AST, Parse } from "../types.js";
3
+
4
+ /**
5
+ * A plugin that records the location of the left parenthesis in CallExpression and
6
+ * NewExpression.
7
+ *
8
+ * This is useful for Language tooling, that we can maps the '(' token from its location,
9
+ * to provide * a signature help for user when they press `(` after a function name.
10
+ *
11
+ * The recorded location will be stored in `lParenLoc` property of the
12
+ * CallExpression/NewExpression node.
13
+ * @returns
14
+ */
15
+ export function recordCallLParenPlugin() {
16
+ return function recordCallLParenPluginTransformer(
17
+ parser: typeof Parser,
18
+ ): typeof Parser {
19
+ return class RecordCallLParenParser extends (parser as typeof Parse.Parser) {
20
+ override parseSubscript(
21
+ base: AST.Expression,
22
+ startPos: number,
23
+ startLoc: AST.Position,
24
+ noCalls?: boolean,
25
+ maybeAsyncArrow?: boolean,
26
+ optionalChained?: boolean,
27
+ forInit?: boolean | "await",
28
+ ): AST.Expression {
29
+ let recordedLParenLoc: AST.SourceLocation | null = null;
30
+ if (!noCalls && this.type === tokTypes.parenL) {
31
+ recordedLParenLoc = {
32
+ start: this.startLoc,
33
+ end: this.endLoc,
34
+ };
35
+ }
36
+ const result = super.parseSubscript(
37
+ base,
38
+ startPos,
39
+ startLoc,
40
+ noCalls,
41
+ maybeAsyncArrow,
42
+ optionalChained,
43
+ forInit,
44
+ );
45
+ if (recordedLParenLoc && result.type === "CallExpression") {
46
+ result.lParenLoc = recordedLParenLoc;
47
+ }
48
+ return result;
49
+ }
50
+
51
+ private _capturedLParenLocFromNew: AST.SourceLocation | null = null;
52
+ private readonly _patchedEat = (type: any) => {
53
+ if (type === tokTypes.parenL) {
54
+ this._capturedLParenLocFromNew = {
55
+ start: this.startLoc,
56
+ end: this.endLoc,
57
+ };
58
+ }
59
+ return this.eat(type);
60
+ };
61
+
62
+ readonly #proxiedThis = new Proxy(this, {
63
+ get: (target, prop) => {
64
+ if (prop === "eat") {
65
+ return this._patchedEat;
66
+ }
67
+ const value = Reflect.get(target, prop);
68
+ if (typeof value === "function") {
69
+ return value.bind(target);
70
+ }
71
+ return value;
72
+ },
73
+ });
74
+
75
+ override parseNew() {
76
+ const result = super.parseNew.apply(this.#proxiedThis);
77
+ if (this._capturedLParenLocFromNew && result.type === "NewExpression") {
78
+ result.lParenLoc = this._capturedLParenLocFromNew;
79
+ this._capturedLParenLocFromNew = null;
80
+ }
81
+ return result;
82
+ }
83
+ };
84
+ };
85
+ }
@@ -1,11 +1,11 @@
1
- export const DEFAULT_SHORTCUT_FUNCTION_PRELUDES = [
2
- "cryo",
3
- "hydro",
4
- "pyro",
5
- "electro",
6
- "anemo",
7
- "geo",
8
- "dendro",
9
- "omni",
10
- ];
11
- export const DEFAULT_QUERY_BINDINGS = ["my", "opp"];
1
+ export const DEFAULT_SHORTCUT_FUNCTION_PRELUDES: string[] = [
2
+ "cryo",
3
+ "hydro",
4
+ "pyro",
5
+ "electro",
6
+ "anemo",
7
+ "geo",
8
+ "dendro",
9
+ "omni",
10
+ ];
11
+ export const DEFAULT_QUERY_BINDINGS: string[] = ["my", "opp"];