jskelet 0.3.5 → 0.4.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.
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Tüm `.jsk` dosyalarını derleyip `.jskelet/templates/` altına yazar.
3
+ */
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { parseTemplate } from "./parse.js";
7
+ import { codegen, normalizeIncludeId } from "./codegen.js";
8
+ import { CompileError } from "./errors.js";
9
+ import {
10
+ collectKnownComponents,
11
+ componentNameFromViewId,
12
+ discoverJskFiles,
13
+ getComponentDirs,
14
+ getViewRoots,
15
+ } from "./resolve.js";
16
+
17
+ /**
18
+ * @param {{ root: string, dirs: Record<string, string> }} config
19
+ * @param {{ quiet?: boolean }} [options]
20
+ * @returns {Promise<{ count: number, manifest: Record<string, string>, outDir: string }>}
21
+ */
22
+ export async function compileAll(config, options = {}) {
23
+ const outDir = path.join(config.dirs.generated, "templates");
24
+ const viewRoots = getViewRoots(config);
25
+ const jskFiles = discoverJskFiles(viewRoots);
26
+ const componentDirs = getComponentDirs(config);
27
+ const knownComponents = collectKnownComponents(componentDirs, jskFiles);
28
+
29
+ // Önceki çıktıyı temizle — silinmiş şablonlar kalmasın.
30
+ fs.rmSync(outDir, { recursive: true, force: true });
31
+ fs.mkdirSync(outDir, { recursive: true });
32
+
33
+ /** @type {Record<string, string>} */
34
+ const manifest = {};
35
+ /** @type {Map<string, { code: string, includes: string[] }>} */
36
+ const compiled = new Map();
37
+
38
+ for (const [viewId, file] of jskFiles) {
39
+ const source = fs.readFileSync(file, "utf8");
40
+ const relative = path.relative(config.root, file).split(path.sep).join("/");
41
+
42
+ let ast;
43
+ try {
44
+ ast = parseTemplate(source, { file: relative });
45
+ } catch (error) {
46
+ if (error instanceof CompileError) throw error;
47
+ throw error;
48
+ }
49
+
50
+ const { code, includes, components: usedComponents } = codegen(ast, {
51
+ viewId,
52
+ file: relative,
53
+ knownComponents: null,
54
+ });
55
+
56
+ for (const name of usedComponents) {
57
+ if (knownComponents.has(name)) continue;
58
+ const camel = name.charAt(0).toLowerCase() + name.slice(1);
59
+ if (knownComponents.has(camel)) continue;
60
+ if (!options.quiet) {
61
+ console.warn(`[jsk] ${relative}: component "${name}" was not found at compile time`);
62
+ }
63
+ }
64
+
65
+ compiled.set(viewId, { code, includes });
66
+ }
67
+
68
+ // Include hedefleri mevcut mu?
69
+ for (const [viewId, { includes }] of compiled) {
70
+ for (const id of includes) {
71
+ const normalized = normalizeIncludeId(id);
72
+ if (!compiled.has(normalized) && !jskFiles.has(normalized)) {
73
+ const file = jskFiles.get(viewId);
74
+ const relative = file
75
+ ? path.relative(config.root, file).split(path.sep).join("/")
76
+ : viewId;
77
+ throw new CompileError(`Unknown include "${id}"`, { file: relative });
78
+ }
79
+ }
80
+ }
81
+
82
+ for (const [viewId, { code }] of compiled) {
83
+ const outFile = path.join(outDir, `${viewId}.mjs`);
84
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
85
+ fs.writeFileSync(outFile, code, "utf8");
86
+ manifest[viewId] = `${viewId}.mjs`.split(path.sep).join("/");
87
+ }
88
+
89
+ const manifestPath = path.join(outDir, "manifest.json");
90
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
91
+
92
+ // Bileşen adı → view id haritası (runtime kayıt için)
93
+ /** @type {Record<string, string>} */
94
+ const components = {};
95
+ for (const viewId of Object.keys(manifest)) {
96
+ const name = componentNameFromViewId(viewId);
97
+ if (name) components[name] = viewId;
98
+ }
99
+ fs.writeFileSync(
100
+ path.join(outDir, "components.json"),
101
+ `${JSON.stringify(components, null, 2)}\n`,
102
+ "utf8",
103
+ );
104
+
105
+ return { count: compiled.size, manifest, outDir };
106
+ }
107
+
108
+ /**
109
+ * Manifest yoksa veya herhangi bir `.jsk` daha yeniyse yeniden derler.
110
+ * Dev sunucu restart'ında build watch kaçırmış olsa bile şablonlar güncel kalır.
111
+ *
112
+ * @param {{ root: string, dirs: Record<string, string> }} config
113
+ * @returns {Promise<{ count: number, rebuilt: boolean }>}
114
+ */
115
+ export async function ensureTemplatesCompiled(config) {
116
+ const viewRoots = getViewRoots(config);
117
+ const jskFiles = discoverJskFiles(viewRoots);
118
+ if (jskFiles.size === 0) {
119
+ return { count: 0, rebuilt: false };
120
+ }
121
+
122
+ const outDir = path.join(config.dirs.generated, "templates");
123
+ const manifestPath = path.join(outDir, "manifest.json");
124
+ let needs = !fs.existsSync(manifestPath);
125
+
126
+ if (!needs) {
127
+ const manifestMtime = fs.statSync(manifestPath).mtimeMs;
128
+ for (const file of jskFiles.values()) {
129
+ if (fs.statSync(file).mtimeMs > manifestMtime) {
130
+ needs = true;
131
+ break;
132
+ }
133
+ }
134
+ }
135
+
136
+ if (!needs) {
137
+ return { count: jskFiles.size, rebuilt: false };
138
+ }
139
+
140
+ const result = await compileAll(config, { quiet: true });
141
+ return { count: result.count, rebuilt: true };
142
+ }
143
+
144
+ /**
145
+ * Tek kaynak dizgisini derler (birim testleri).
146
+ * @param {string} source
147
+ * @param {{ viewId?: string, file?: string, knownComponents?: Set<string> }} [options]
148
+ * @returns {{ code: string, includes: string[], components: string[] }}
149
+ */
150
+ export function compileSource(source, options = {}) {
151
+ const viewId = options.viewId ?? "test";
152
+ const ast = parseTemplate(source, { file: options.file ?? "test.jsk" });
153
+ return codegen(ast, {
154
+ viewId,
155
+ file: options.file ?? "test.jsk",
156
+ knownComponents: options.knownComponents ?? null,
157
+ });
158
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Şablon derleme hataları. Satır/sütun korunur ki DX overlay ve CLI
3
+ * çıktısı kaynağa işaret edebilsin.
4
+ */
5
+
6
+ /**
7
+ * @param {string} source
8
+ * @param {number} index
9
+ * @returns {{ line: number, column: number }}
10
+ */
11
+ export function indexToLocation(source, index) {
12
+ let line = 1;
13
+ let column = 1;
14
+ const end = Math.min(index, source.length);
15
+ for (let i = 0; i < end; i++) {
16
+ if (source[i] === "\n") {
17
+ line += 1;
18
+ column = 1;
19
+ } else {
20
+ column += 1;
21
+ }
22
+ }
23
+ return { line, column };
24
+ }
25
+
26
+ /**
27
+ * @param {string} source
28
+ * @param {number} line
29
+ * @returns {string}
30
+ */
31
+ function lineSnippet(source, line) {
32
+ const lines = source.split(/\r?\n/);
33
+ return lines[line - 1] ?? "";
34
+ }
35
+
36
+ export class CompileError extends Error {
37
+ /**
38
+ * @param {string} message
39
+ * @param {{ file?: string, source?: string, index?: number,
40
+ * line?: number, column?: number }} [opts]
41
+ */
42
+ constructor(message, opts = {}) {
43
+ let line = opts.line;
44
+ let column = opts.column;
45
+ if ((line == null || column == null) && opts.source != null && opts.index != null) {
46
+ const loc = indexToLocation(opts.source, opts.index);
47
+ line = loc.line;
48
+ column = loc.column;
49
+ }
50
+
51
+ const where =
52
+ opts.file && line != null
53
+ ? `${opts.file}:${line}:${column ?? 1}`
54
+ : opts.file ?? "";
55
+ const snippet =
56
+ opts.source && line != null ? lineSnippet(opts.source, line).trimEnd() : "";
57
+ const head = where ? `${where}\n` : "";
58
+ const body = snippet ? `${message}\n\n ${snippet}\n ${" ".repeat((column ?? 1) - 1)}^` : message;
59
+
60
+ super(`${head}${body}`);
61
+ this.name = "CompileError";
62
+ this.file = opts.file;
63
+ this.line = line;
64
+ this.column = column;
65
+ }
66
+ }
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Şablon ifadeleri — kısıtlı alt küme. İstek anında yorumlanmaz; build'de
3
+ * JS kaynak metnine çevrilir. Çağrı, atama ve `new` yok.
4
+ */
5
+ import { CompileError } from "./errors.js";
6
+
7
+ /**
8
+ * @typedef {{ type: 'Literal', value: string | number | boolean | null, raw: string }} LiteralNode
9
+ * @typedef {{ type: 'Ident', name: string }} IdentNode
10
+ * @typedef {{ type: 'Member', object: ExprNode, property: string, optional: boolean }} MemberNode
11
+ * @typedef {{ type: 'Index', object: ExprNode, index: ExprNode, optional: boolean }} IndexNode
12
+ * @typedef {{ type: 'Unary', op: '!' | '-', argument: ExprNode }} UnaryNode
13
+ * @typedef {{ type: 'Binary', op: string, left: ExprNode, right: ExprNode }} BinaryNode
14
+ * @typedef {{ type: 'Cond', test: ExprNode, consequent: ExprNode, alternate: ExprNode }} CondNode
15
+ * @typedef {LiteralNode | IdentNode | MemberNode | IndexNode | UnaryNode | BinaryNode | CondNode} ExprNode
16
+ */
17
+
18
+ const IDENT_START = /[A-Za-z_$]/
19
+ const IDENT_CONT = /[A-Za-z0-9_$]/
20
+
21
+ /**
22
+ * @param {string} source
23
+ * @param {number} start
24
+ * @param {number} end
25
+ * @param {{ file?: string, templateSource?: string, templateIndex?: number }} [meta]
26
+ * @returns {ExprNode}
27
+ */
28
+ export function parseExpr(source, start = 0, end = source.length, meta = {}) {
29
+ const parser = new ExprParser(source, start, end, meta);
30
+ const node = parser.parseCond();
31
+ parser.skipWs();
32
+ if (parser.i < parser.end) {
33
+ throw parser.error(`Unexpected token in expression`);
34
+ }
35
+ return node;
36
+ }
37
+
38
+ /**
39
+ * İfadeyi JS kaynak metnine çevirir. `locals` içindeki isimler çıplak kalır;
40
+ * diğerleri `data.` öneki alır.
41
+ *
42
+ * @param {ExprNode} node
43
+ * @param {Set<string>} [locals]
44
+ * @returns {string}
45
+ */
46
+ export function codegenExpr(node, locals = new Set()) {
47
+ switch (node.type) {
48
+ case "Literal":
49
+ return node.raw;
50
+ case "Ident":
51
+ if (locals.has(node.name)) return node.name;
52
+ return `data.${node.name}`;
53
+ case "Member": {
54
+ const obj = codegenExpr(node.object, locals);
55
+ const op = node.optional ? "?." : ".";
56
+ return `${obj}${op}${node.property}`;
57
+ }
58
+ case "Index": {
59
+ const obj = codegenExpr(node.object, locals);
60
+ const idx = codegenExpr(node.index, locals);
61
+ const op = node.optional ? "?.[" : "[";
62
+ return `${obj}${op}${idx}]`;
63
+ }
64
+ case "Unary":
65
+ return `(${node.op}${codegenExpr(node.argument, locals)})`;
66
+ case "Binary":
67
+ return `(${codegenExpr(node.left, locals)} ${node.op} ${codegenExpr(node.right, locals)})`;
68
+ case "Cond":
69
+ return `(${codegenExpr(node.test, locals)} ? ${codegenExpr(node.consequent, locals)} : ${codegenExpr(node.alternate, locals)})`;
70
+ default:
71
+ throw new Error(`Unknown expr node: ${/** @type {{ type: string }} */ (node).type}`);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * İfadede geçen kök tanımlayıcılar (data alanları / local adayları).
77
+ * @param {ExprNode} node
78
+ * @param {Set<string>} [out]
79
+ * @returns {Set<string>}
80
+ */
81
+ export function collectIdents(node, out = new Set()) {
82
+ switch (node.type) {
83
+ case "Literal":
84
+ break;
85
+ case "Ident":
86
+ out.add(node.name);
87
+ break;
88
+ case "Member":
89
+ collectIdents(node.object, out);
90
+ break;
91
+ case "Index":
92
+ collectIdents(node.object, out);
93
+ collectIdents(node.index, out);
94
+ break;
95
+ case "Unary":
96
+ collectIdents(node.argument, out);
97
+ break;
98
+ case "Binary":
99
+ collectIdents(node.left, out);
100
+ collectIdents(node.right, out);
101
+ break;
102
+ case "Cond":
103
+ collectIdents(node.test, out);
104
+ collectIdents(node.consequent, out);
105
+ collectIdents(node.alternate, out);
106
+ break;
107
+ default:
108
+ break;
109
+ }
110
+ return out;
111
+ }
112
+
113
+ class ExprParser {
114
+ /**
115
+ * @param {string} source
116
+ * @param {number} start
117
+ * @param {number} end
118
+ * @param {{ file?: string, templateSource?: string, templateIndex?: number }} meta
119
+ */
120
+ constructor(source, start, end, meta) {
121
+ this.source = source;
122
+ this.i = start;
123
+ this.end = end;
124
+ this.meta = meta;
125
+ this.base = start;
126
+ }
127
+
128
+ /**
129
+ * @param {string} message
130
+ * @returns {CompileError}
131
+ */
132
+ error(message) {
133
+ const offset = this.meta.templateIndex != null ? this.meta.templateIndex + (this.i - this.base) : this.i;
134
+ return new CompileError(message, {
135
+ file: this.meta.file,
136
+ source: this.meta.templateSource ?? this.source,
137
+ index: this.meta.templateSource ? offset : this.i,
138
+ });
139
+ }
140
+
141
+ skipWs() {
142
+ while (this.i < this.end && /\s/.test(this.source[this.i])) this.i += 1;
143
+ }
144
+
145
+ peek() {
146
+ return this.i < this.end ? this.source[this.i] : "";
147
+ }
148
+
149
+ /**
150
+ * @param {string} expected
151
+ */
152
+ eat(expected) {
153
+ this.skipWs();
154
+ if (!this.source.startsWith(expected, this.i)) {
155
+ throw this.error(`Expected '${expected}'`);
156
+ }
157
+ this.i += expected.length;
158
+ }
159
+
160
+ /**
161
+ * @param {string} expected
162
+ * @returns {boolean}
163
+ */
164
+ match(expected) {
165
+ this.skipWs();
166
+ if (!this.source.startsWith(expected, this.i)) return false;
167
+ this.i += expected.length;
168
+ return true;
169
+ }
170
+
171
+ /** @returns {ExprNode} */
172
+ parseCond() {
173
+ const test = this.parseOr();
174
+ this.skipWs();
175
+ if (!this.match("?")) return test;
176
+ const consequent = this.parseCond();
177
+ this.eat(":");
178
+ const alternate = this.parseCond();
179
+ return { type: "Cond", test, consequent, alternate };
180
+ }
181
+
182
+ /** @returns {ExprNode} */
183
+ parseOr() {
184
+ let left = this.parseAnd();
185
+ for (;;) {
186
+ this.skipWs();
187
+ if (!this.match("||")) break;
188
+ left = { type: "Binary", op: "||", left, right: this.parseAnd() };
189
+ }
190
+ return left;
191
+ }
192
+
193
+ /** @returns {ExprNode} */
194
+ parseAnd() {
195
+ let left = this.parseEquality();
196
+ for (;;) {
197
+ this.skipWs();
198
+ if (!this.match("&&")) break;
199
+ left = { type: "Binary", op: "&&", left, right: this.parseEquality() };
200
+ }
201
+ return left;
202
+ }
203
+
204
+ /** @returns {ExprNode} */
205
+ parseEquality() {
206
+ let left = this.parseCompare();
207
+ for (;;) {
208
+ this.skipWs();
209
+ let op = null;
210
+ if (this.match("===")) op = "===";
211
+ else if (this.match("!==")) op = "!==";
212
+ else if (this.match("==")) op = "==";
213
+ else if (this.match("!=")) op = "!=";
214
+ if (!op) break;
215
+ left = { type: "Binary", op, left, right: this.parseCompare() };
216
+ }
217
+ return left;
218
+ }
219
+
220
+ /** @returns {ExprNode} */
221
+ parseCompare() {
222
+ let left = this.parseAdd();
223
+ for (;;) {
224
+ this.skipWs();
225
+ let op = null;
226
+ if (this.match("<=")) op = "<=";
227
+ else if (this.match(">=")) op = ">=";
228
+ else if (this.match("<")) op = "<";
229
+ else if (this.match(">")) op = ">";
230
+ if (!op) break;
231
+ left = { type: "Binary", op, left, right: this.parseAdd() };
232
+ }
233
+ return left;
234
+ }
235
+
236
+ /** @returns {ExprNode} */
237
+ parseAdd() {
238
+ let left = this.parseMul();
239
+ for (;;) {
240
+ this.skipWs();
241
+ const ch = this.peek();
242
+ if (ch !== "+" && ch !== "-") break;
243
+ // `?.` / sayılar ile karışmasın diye yalnızca tek karakter op.
244
+ this.i += 1;
245
+ left = { type: "Binary", op: ch, left, right: this.parseMul() };
246
+ }
247
+ return left;
248
+ }
249
+
250
+ /** @returns {ExprNode} */
251
+ parseMul() {
252
+ let left = this.parseUnary();
253
+ for (;;) {
254
+ this.skipWs();
255
+ const ch = this.peek();
256
+ if (ch !== "*" && ch !== "/" && ch !== "%") break;
257
+ this.i += 1;
258
+ left = { type: "Binary", op: ch, left, right: this.parseUnary() };
259
+ }
260
+ return left;
261
+ }
262
+
263
+ /** @returns {ExprNode} */
264
+ parseUnary() {
265
+ this.skipWs();
266
+ if (this.match("!")) {
267
+ return { type: "Unary", op: "!", argument: this.parseUnary() };
268
+ }
269
+ if (this.peek() === "-" && !/[0-9.]/.test(this.source[this.i + 1] ?? "")) {
270
+ this.i += 1;
271
+ return { type: "Unary", op: "-", argument: this.parseUnary() };
272
+ }
273
+ return this.parsePostfix();
274
+ }
275
+
276
+ /** @returns {ExprNode} */
277
+ parsePostfix() {
278
+ let node = this.parsePrimary();
279
+ for (;;) {
280
+ this.skipWs();
281
+ if (this.match("?.")) {
282
+ this.skipWs();
283
+ if (this.peek() === "[") {
284
+ this.i += 1;
285
+ const index = this.parseCond();
286
+ this.eat("]");
287
+ node = { type: "Index", object: node, index, optional: true };
288
+ continue;
289
+ }
290
+ const property = this.readIdent();
291
+ node = { type: "Member", object: node, property, optional: true };
292
+ continue;
293
+ }
294
+ if (this.peek() === ".") {
295
+ this.i += 1;
296
+ const property = this.readIdent();
297
+ node = { type: "Member", object: node, property, optional: false };
298
+ continue;
299
+ }
300
+ if (this.peek() === "[") {
301
+ this.i += 1;
302
+ const index = this.parseCond();
303
+ this.eat("]");
304
+ node = { type: "Index", object: node, index, optional: false };
305
+ continue;
306
+ }
307
+ break;
308
+ }
309
+ return node;
310
+ }
311
+
312
+ /** @returns {ExprNode} */
313
+ parsePrimary() {
314
+ this.skipWs();
315
+ const ch = this.peek();
316
+
317
+ if (ch === "(") {
318
+ this.i += 1;
319
+ const node = this.parseCond();
320
+ this.eat(")");
321
+ return node;
322
+ }
323
+
324
+ if (ch === '"' || ch === "'") {
325
+ return this.readString();
326
+ }
327
+
328
+ if (ch === "`") {
329
+ throw this.error("Template literals are not allowed in expressions");
330
+ }
331
+
332
+ if (/[0-9]/.test(ch) || (ch === "." && /[0-9]/.test(this.source[this.i + 1] ?? ""))) {
333
+ return this.readNumber();
334
+ }
335
+
336
+ if (IDENT_START.test(ch)) {
337
+ const name = this.readIdent();
338
+ if (name === "true") return { type: "Literal", value: true, raw: "true" };
339
+ if (name === "false") return { type: "Literal", value: false, raw: "false" };
340
+ if (name === "null") return { type: "Literal", value: null, raw: "null" };
341
+ return { type: "Ident", name };
342
+ }
343
+
344
+ throw this.error("Expected expression");
345
+ }
346
+
347
+ /** @returns {string} */
348
+ readIdent() {
349
+ this.skipWs();
350
+ if (!IDENT_START.test(this.peek())) {
351
+ throw this.error("Expected identifier");
352
+ }
353
+ const start = this.i;
354
+ this.i += 1;
355
+ while (this.i < this.end && IDENT_CONT.test(this.source[this.i])) this.i += 1;
356
+ return this.source.slice(start, this.i);
357
+ }
358
+
359
+ /** @returns {LiteralNode} */
360
+ readString() {
361
+ const quote = this.source[this.i];
362
+ this.i += 1;
363
+ let raw = quote;
364
+ let value = "";
365
+ while (this.i < this.end) {
366
+ const ch = this.source[this.i];
367
+ if (ch === "\\") {
368
+ raw += ch + (this.source[this.i + 1] ?? "");
369
+ value += this.source[this.i + 1] ?? "";
370
+ this.i += 2;
371
+ continue;
372
+ }
373
+ if (ch === quote) {
374
+ raw += quote;
375
+ this.i += 1;
376
+ return { type: "Literal", value, raw };
377
+ }
378
+ if (ch === "\n") {
379
+ throw this.error("Unterminated string");
380
+ }
381
+ raw += ch;
382
+ value += ch;
383
+ this.i += 1;
384
+ }
385
+ throw this.error("Unterminated string");
386
+ }
387
+
388
+ /** @returns {LiteralNode} */
389
+ readNumber() {
390
+ const start = this.i;
391
+ while (this.i < this.end && /[0-9_]/.test(this.source[this.i])) this.i += 1;
392
+ if (this.source[this.i] === ".") {
393
+ this.i += 1;
394
+ while (this.i < this.end && /[0-9_]/.test(this.source[this.i])) this.i += 1;
395
+ }
396
+ if (this.source[this.i] === "e" || this.source[this.i] === "E") {
397
+ this.i += 1;
398
+ if (this.source[this.i] === "+" || this.source[this.i] === "-") this.i += 1;
399
+ while (this.i < this.end && /[0-9]/.test(this.source[this.i])) this.i += 1;
400
+ }
401
+ const raw = this.source.slice(start, this.i).replace(/_/g, "");
402
+ return { type: "Literal", value: Number(raw), raw };
403
+ }
404
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Build-time `.jsk` derleyici yüzeyi.
3
+ */
4
+ export { CompileError, indexToLocation } from "./errors.js";
5
+ export { parseExpr, codegenExpr } from "./expr.js";
6
+ export { parseTemplate, isComponentTag } from "./parse.js";
7
+ export { codegen, toPascalCase, normalizeIncludeId } from "./codegen.js";
8
+ export {
9
+ getViewRoots,
10
+ getComponentDirs,
11
+ discoverJskFiles,
12
+ componentNameFromViewId,
13
+ collectKnownComponents,
14
+ toComponentTag,
15
+ } from "./resolve.js";
16
+ export { scanNamedExports } from "./scan-exports.js";
17
+ export { compileAll, compileSource, ensureTemplatesCompiled } from "./compile-all.js";