critique 0.1.121 → 0.1.122
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/dist/cli.js +1 -1
- package/dist/review/review-app.test.js +3 -3
- package/dist/web-utils.d.ts.map +1 -1
- package/dist/web-utils.js +60 -26
- package/package.json +3 -3
- package/src/cli.tsx +1 -1
- package/src/review/review-app.test.tsx +3 -3
- package/src/web-utils.tsx +66 -26
- package/src/.test-stdin-pager-tmp/binary.diff +0 -3
- package/src/.test-stdin-pager-tmp/colored.diff +0 -8
- package/src/.test-stdin-pager-tmp/context-only.diff +0 -7
- package/src/.test-stdin-pager-tmp/delete.diff +0 -9
- package/src/.test-stdin-pager-tmp/empty.diff +0 -0
- package/src/.test-stdin-pager-tmp/large.diff +0 -19
- package/src/.test-stdin-pager-tmp/multi.diff +0 -20
- package/src/.test-stdin-pager-tmp/newfile.diff +0 -12
- package/src/.test-stdin-pager-tmp/rename.diff +0 -11
- package/src/.test-stdin-pager-tmp/single.diff +0 -8
package/dist/cli.js
CHANGED
|
@@ -937,7 +937,7 @@ async function runWebMode(diffContent, options) {
|
|
|
937
937
|
const { renderDiffToOgImage } = await import("./image.js");
|
|
938
938
|
const ogImg = await renderDiffToOgImage(diffContent, {
|
|
939
939
|
themeName: "github-light",
|
|
940
|
-
stabilizeMs:
|
|
940
|
+
stabilizeMs: 2000,
|
|
941
941
|
});
|
|
942
942
|
if (ogImg) {
|
|
943
943
|
await uploadOgImage(result.id, ogImg);
|
|
@@ -791,11 +791,11 @@ The diagram above should not wrap.`,
|
|
|
791
791
|
Architecture
|
|
792
792
|
|
|
793
793
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
794
|
-
|
|
794
|
+
┌─────────────┐
|
|
795
795
|
│ Client │────▶│ Router │────▶│ Handler │────▶│
|
|
796
|
-
|
|
796
|
+
Database │
|
|
797
797
|
└─────────────┘ └─────────────┘ └─────────────┘
|
|
798
|
-
|
|
798
|
+
└─────────────┘
|
|
799
799
|
|
|
800
800
|
|
|
801
801
|
src/config.ts +1-0
|
package/dist/web-utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-utils.d.ts","sourceRoot":"","sources":["../src/web-utils.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAiB,aAAa,EAAE,YAAY,EAA+B,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"web-utils.d.ts","sourceRoot":"","sources":["../src/web-utils.tsx"],"names":[],"mappings":"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"}
|
package/dist/web-utils.js
CHANGED
|
@@ -8,6 +8,7 @@ import fs from "fs";
|
|
|
8
8
|
import { tmpdir } from "os";
|
|
9
9
|
import { join } from "path";
|
|
10
10
|
import { getResolvedTheme, rgbaToHex } from "./themes.js";
|
|
11
|
+
import { DiffRenderable } from "@opentuah/core";
|
|
11
12
|
import { loadStoredLicenseKey, loadOrCreateOwnerSecret } from "./license.js";
|
|
12
13
|
const execAsync = promisify(exec);
|
|
13
14
|
// Worker URL for uploading HTML previews
|
|
@@ -41,39 +42,70 @@ function getContentHeight(root) {
|
|
|
41
42
|
return Math.ceil(maxBottom);
|
|
42
43
|
}
|
|
43
44
|
/**
|
|
44
|
-
*
|
|
45
|
+
* Find all DiffRenderable instances in the renderer tree.
|
|
46
|
+
* Walks the tree recursively checking instanceof DiffRenderable.
|
|
47
|
+
*/
|
|
48
|
+
function findDiffRenderables(root) {
|
|
49
|
+
const results = [];
|
|
50
|
+
function walk(node) {
|
|
51
|
+
if (!node.getChildren)
|
|
52
|
+
return;
|
|
53
|
+
for (const child of node.getChildren()) {
|
|
54
|
+
if (child instanceof DiffRenderable) {
|
|
55
|
+
results.push(child);
|
|
56
|
+
}
|
|
57
|
+
walk(child);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
walk(root);
|
|
61
|
+
return results;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Wait for tree-sitter syntax highlighting to complete on all diff elements,
|
|
65
|
+
* then wait for rendering to stabilize (no more requestRender calls).
|
|
45
66
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
67
|
+
* Two-phase approach:
|
|
68
|
+
* 1. Wait for DiffRenderable.isHighlighting to become false on all diffs
|
|
69
|
+
* (deterministic, exits as soon as tree-sitter is done)
|
|
70
|
+
* 2. Wait for render idle — no new requestRender calls for idleMs
|
|
71
|
+
* (catches deferred rebuilds like DiffRenderable.requestRebuild which
|
|
72
|
+
* uses queueMicrotask to schedule buildView + requestRender after
|
|
73
|
+
* highlighting completes)
|
|
50
74
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
75
|
+
* Phase 2 is critical because DiffRenderable's split view rebuild happens
|
|
76
|
+
* asynchronously via microtask AFTER isHighlighting goes false. Without it,
|
|
77
|
+
* the captured frame may have concealed (unhighlighted) content on one side.
|
|
54
78
|
*/
|
|
55
|
-
async function
|
|
56
|
-
|
|
57
|
-
// - Interactive TUI (500ms): idleMs=200, maxMs=800
|
|
58
|
-
// - Web/batch (100ms): idleMs=80, maxMs=400
|
|
59
|
-
const idleMs = Math.max(Math.round(stabilizeMs * 0.4), 60);
|
|
60
|
-
const maxMs = Math.max(stabilizeMs, Math.round(stabilizeMs * 4));
|
|
79
|
+
async function waitForHighlightAndRenderStabilization(renderer, renderOnce, maxMs = 2000) {
|
|
80
|
+
const startTime = Date.now();
|
|
61
81
|
const pollMs = 20;
|
|
82
|
+
const idleMs = 80;
|
|
83
|
+
// Track render requests to detect when rendering has quiesced
|
|
62
84
|
let lastRenderTime = Date.now();
|
|
63
|
-
const startTime = lastRenderTime;
|
|
64
85
|
const originalRequestRender = renderer.root.requestRender.bind(renderer.root);
|
|
65
86
|
renderer.root.requestRender = function () {
|
|
66
87
|
lastRenderTime = Date.now();
|
|
67
88
|
originalRequestRender();
|
|
68
89
|
};
|
|
69
|
-
|
|
90
|
+
// Do one render cycle to kick off highlighting
|
|
91
|
+
await renderOnce();
|
|
92
|
+
// Phase 1: wait for isHighlighting to become false on all diffs
|
|
93
|
+
while (Date.now() - startTime < maxMs) {
|
|
94
|
+
const diffs = findDiffRenderables(renderer.root);
|
|
95
|
+
if (diffs.length === 0 || diffs.every(d => !d.isHighlighting)) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
99
|
+
await renderOnce();
|
|
100
|
+
}
|
|
101
|
+
// Phase 2: wait for render to stabilize (catches deferred rebuilds)
|
|
102
|
+
// Reset the render timestamp so we wait for any post-highlight renders
|
|
103
|
+
lastRenderTime = Date.now();
|
|
104
|
+
await renderOnce();
|
|
105
|
+
while (Date.now() - startTime < maxMs) {
|
|
70
106
|
const now = Date.now();
|
|
71
|
-
// Exit if idle long enough (no render requests for idleMs)
|
|
72
107
|
if (now - lastRenderTime >= idleMs)
|
|
73
108
|
break;
|
|
74
|
-
// Exit if hard cap reached
|
|
75
|
-
if (now - startTime >= maxMs)
|
|
76
|
-
break;
|
|
77
109
|
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
78
110
|
await renderOnce();
|
|
79
111
|
}
|
|
@@ -203,8 +235,8 @@ async function renderDiffToFrameWithSectionPositions(diffContent, options) {
|
|
|
203
235
|
resize(options.cols, finalHeight);
|
|
204
236
|
await renderOnce();
|
|
205
237
|
}
|
|
206
|
-
// Wait for
|
|
207
|
-
await
|
|
238
|
+
// Wait for tree-sitter highlighting + render stabilization
|
|
239
|
+
await waitForHighlightAndRenderStabilization(renderer, renderOnce, options.stabilizeMs ?? 2000);
|
|
208
240
|
const sectionPositions = [];
|
|
209
241
|
for (let idx = 0; idx < fileNames.length; idx++) {
|
|
210
242
|
const section = fileSectionRefs.get(idx);
|
|
@@ -419,7 +451,9 @@ export async function captureResponsiveHtml(diffContent, options) {
|
|
|
419
451
|
// These act as upper bounds to prevent runaway memory usage
|
|
420
452
|
const desktopRows = Math.max(options.baseRows * 3, 5000);
|
|
421
453
|
const mobileRows = Math.max(Math.ceil(desktopRows * (options.desktopCols / options.mobileCols)), 10000);
|
|
422
|
-
|
|
454
|
+
// With deterministic isHighlighting detection, stabilizeMs is just a safety cap.
|
|
455
|
+
// The function exits instantly when highlighting completes, so 2000ms is fine.
|
|
456
|
+
const stabilizeMs = options.stabilizeMs ?? 2000;
|
|
423
457
|
// Run all renders in parallel: desktop HTML, mobile HTML, and OG image
|
|
424
458
|
const ogImagePromise = options.skipOgImage
|
|
425
459
|
? Promise.resolve(null)
|
|
@@ -532,8 +566,8 @@ export async function renderReviewToFrame(options) {
|
|
|
532
566
|
resize(options.cols, finalHeight);
|
|
533
567
|
await renderOnce();
|
|
534
568
|
}
|
|
535
|
-
// Wait for
|
|
536
|
-
await
|
|
569
|
+
// Wait for tree-sitter highlighting + render stabilization
|
|
570
|
+
await waitForHighlightAndRenderStabilization(renderer, renderOnce, options.stabilizeMs ?? 2000);
|
|
537
571
|
// Capture the final frame
|
|
538
572
|
const buffer = renderer.currentRenderBuffer;
|
|
539
573
|
const cursorState = renderer.getCursorState();
|
|
@@ -575,7 +609,7 @@ export async function captureReviewResponsiveHtml(options) {
|
|
|
575
609
|
// These act as upper bounds to prevent runaway memory usage
|
|
576
610
|
const desktopRows = Math.max(options.baseRows * 3, 5000);
|
|
577
611
|
const mobileRows = Math.max(Math.ceil(desktopRows * (options.desktopCols / options.mobileCols)), 10000);
|
|
578
|
-
const stabilizeMs = options.stabilizeMs ??
|
|
612
|
+
const stabilizeMs = options.stabilizeMs ?? 2000;
|
|
579
613
|
// Generate OG image from first few hunks' raw diff (in parallel with HTML renders)
|
|
580
614
|
const ogImagePromise = options.skipOgImage
|
|
581
615
|
? Promise.resolve(null)
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "critique",
|
|
3
3
|
"module": "src/diff.tsx",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.122",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
8
8
|
"bin": "./dist/cli.js",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@agentclientprotocol/sdk": "^0.13.1",
|
|
46
46
|
"@clack/prompts": "1.0.0-alpha.9",
|
|
47
|
-
"@opentuah/core": "0.1.
|
|
48
|
-
"@opentuah/react": "0.1.
|
|
47
|
+
"@opentuah/core": "0.1.97",
|
|
48
|
+
"@opentuah/react": "0.1.97",
|
|
49
49
|
"@parcel/watcher": "^2.5.6",
|
|
50
50
|
"diff": "^8.0.2",
|
|
51
51
|
"goke": "^6.1.3",
|
package/src/cli.tsx
CHANGED
|
@@ -1187,7 +1187,7 @@ async function runWebMode(
|
|
|
1187
1187
|
const { renderDiffToOgImage } = await import("./image.js");
|
|
1188
1188
|
const ogImg = await renderDiffToOgImage(diffContent, {
|
|
1189
1189
|
themeName: "github-light",
|
|
1190
|
-
stabilizeMs:
|
|
1190
|
+
stabilizeMs: 2000,
|
|
1191
1191
|
});
|
|
1192
1192
|
if (ogImg) {
|
|
1193
1193
|
await uploadOgImage(result.id, ogImg);
|
|
@@ -928,11 +928,11 @@ The diagram above should not wrap.`,
|
|
|
928
928
|
Architecture
|
|
929
929
|
|
|
930
930
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
931
|
-
|
|
931
|
+
┌─────────────┐
|
|
932
932
|
│ Client │────▶│ Router │────▶│ Handler │────▶│
|
|
933
|
-
|
|
933
|
+
Database │
|
|
934
934
|
└─────────────┘ └─────────────┘ └─────────────┘
|
|
935
|
-
|
|
935
|
+
└─────────────┘
|
|
936
936
|
|
|
937
937
|
|
|
938
938
|
src/config.ts +1-0
|
package/src/web-utils.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { tmpdir } from "os"
|
|
|
9
9
|
import { join } from "path"
|
|
10
10
|
import { getResolvedTheme, rgbaToHex } from "./themes.js"
|
|
11
11
|
import type { BoxRenderable, CapturedFrame, CapturedLine, RootRenderable, CliRenderer } from "@opentuah/core"
|
|
12
|
+
import { DiffRenderable } from "@opentuah/core"
|
|
12
13
|
import type { IndexedHunk, ReviewYaml } from "./review/types.js"
|
|
13
14
|
import { loadStoredLicenseKey, loadOrCreateOwnerSecret } from "./license.js"
|
|
14
15
|
|
|
@@ -93,43 +94,80 @@ function getContentHeight(root: RootRenderable): number {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
/**
|
|
96
|
-
*
|
|
97
|
+
* Find all DiffRenderable instances in the renderer tree.
|
|
98
|
+
* Walks the tree recursively checking instanceof DiffRenderable.
|
|
99
|
+
*/
|
|
100
|
+
function findDiffRenderables(root: RootRenderable): InstanceType<typeof DiffRenderable>[] {
|
|
101
|
+
const results: InstanceType<typeof DiffRenderable>[] = []
|
|
102
|
+
|
|
103
|
+
function walk(node: { getChildren?: () => any[] }) {
|
|
104
|
+
if (!node.getChildren) return
|
|
105
|
+
for (const child of node.getChildren()) {
|
|
106
|
+
if (child instanceof DiffRenderable) {
|
|
107
|
+
results.push(child)
|
|
108
|
+
}
|
|
109
|
+
walk(child)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
walk(root)
|
|
114
|
+
return results
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Wait for tree-sitter syntax highlighting to complete on all diff elements,
|
|
119
|
+
* then wait for rendering to stabilize (no more requestRender calls).
|
|
97
120
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
121
|
+
* Two-phase approach:
|
|
122
|
+
* 1. Wait for DiffRenderable.isHighlighting to become false on all diffs
|
|
123
|
+
* (deterministic, exits as soon as tree-sitter is done)
|
|
124
|
+
* 2. Wait for render idle — no new requestRender calls for idleMs
|
|
125
|
+
* (catches deferred rebuilds like DiffRenderable.requestRebuild which
|
|
126
|
+
* uses queueMicrotask to schedule buildView + requestRender after
|
|
127
|
+
* highlighting completes)
|
|
102
128
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
129
|
+
* Phase 2 is critical because DiffRenderable's split view rebuild happens
|
|
130
|
+
* asynchronously via microtask AFTER isHighlighting goes false. Without it,
|
|
131
|
+
* the captured frame may have concealed (unhighlighted) content on one side.
|
|
106
132
|
*/
|
|
107
|
-
async function
|
|
133
|
+
async function waitForHighlightAndRenderStabilization(
|
|
108
134
|
renderer: CliRenderer,
|
|
109
135
|
renderOnce: () => Promise<void>,
|
|
110
|
-
|
|
136
|
+
maxMs: number = 2000
|
|
111
137
|
): Promise<void> {
|
|
112
|
-
|
|
113
|
-
// - Interactive TUI (500ms): idleMs=200, maxMs=800
|
|
114
|
-
// - Web/batch (100ms): idleMs=80, maxMs=400
|
|
115
|
-
const idleMs = Math.max(Math.round(stabilizeMs * 0.4), 60)
|
|
116
|
-
const maxMs = Math.max(stabilizeMs, Math.round(stabilizeMs * 4))
|
|
138
|
+
const startTime = Date.now()
|
|
117
139
|
const pollMs = 20
|
|
140
|
+
const idleMs = 80
|
|
118
141
|
|
|
142
|
+
// Track render requests to detect when rendering has quiesced
|
|
119
143
|
let lastRenderTime = Date.now()
|
|
120
|
-
const startTime = lastRenderTime
|
|
121
144
|
const originalRequestRender = renderer.root.requestRender.bind(renderer.root)
|
|
122
145
|
renderer.root.requestRender = function() {
|
|
123
146
|
lastRenderTime = Date.now()
|
|
124
147
|
originalRequestRender()
|
|
125
148
|
}
|
|
126
149
|
|
|
127
|
-
|
|
150
|
+
// Do one render cycle to kick off highlighting
|
|
151
|
+
await renderOnce()
|
|
152
|
+
|
|
153
|
+
// Phase 1: wait for isHighlighting to become false on all diffs
|
|
154
|
+
while (Date.now() - startTime < maxMs) {
|
|
155
|
+
const diffs = findDiffRenderables(renderer.root)
|
|
156
|
+
if (diffs.length === 0 || diffs.every(d => !d.isHighlighting)) {
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
await new Promise(resolve => setTimeout(resolve, pollMs))
|
|
160
|
+
await renderOnce()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Phase 2: wait for render to stabilize (catches deferred rebuilds)
|
|
164
|
+
// Reset the render timestamp so we wait for any post-highlight renders
|
|
165
|
+
lastRenderTime = Date.now()
|
|
166
|
+
await renderOnce()
|
|
167
|
+
|
|
168
|
+
while (Date.now() - startTime < maxMs) {
|
|
128
169
|
const now = Date.now()
|
|
129
|
-
// Exit if idle long enough (no render requests for idleMs)
|
|
130
170
|
if (now - lastRenderTime >= idleMs) break
|
|
131
|
-
// Exit if hard cap reached
|
|
132
|
-
if (now - startTime >= maxMs) break
|
|
133
171
|
await new Promise(resolve => setTimeout(resolve, pollMs))
|
|
134
172
|
await renderOnce()
|
|
135
173
|
}
|
|
@@ -315,8 +353,8 @@ async function renderDiffToFrameWithSectionPositions(
|
|
|
315
353
|
await renderOnce()
|
|
316
354
|
}
|
|
317
355
|
|
|
318
|
-
// Wait for
|
|
319
|
-
await
|
|
356
|
+
// Wait for tree-sitter highlighting + render stabilization
|
|
357
|
+
await waitForHighlightAndRenderStabilization(renderer, renderOnce, options.stabilizeMs ?? 2000)
|
|
320
358
|
|
|
321
359
|
const sectionPositions: FileSectionPosition[] = []
|
|
322
360
|
for (let idx = 0; idx < fileNames.length; idx++) {
|
|
@@ -585,7 +623,9 @@ export async function captureResponsiveHtml(
|
|
|
585
623
|
// These act as upper bounds to prevent runaway memory usage
|
|
586
624
|
const desktopRows = Math.max(options.baseRows * 3, 5000)
|
|
587
625
|
const mobileRows = Math.max(Math.ceil(desktopRows * (options.desktopCols / options.mobileCols)), 10000)
|
|
588
|
-
|
|
626
|
+
// With deterministic isHighlighting detection, stabilizeMs is just a safety cap.
|
|
627
|
+
// The function exits instantly when highlighting completes, so 2000ms is fine.
|
|
628
|
+
const stabilizeMs = options.stabilizeMs ?? 2000
|
|
589
629
|
|
|
590
630
|
// Run all renders in parallel: desktop HTML, mobile HTML, and OG image
|
|
591
631
|
const ogImagePromise = options.skipOgImage
|
|
@@ -725,8 +765,8 @@ export async function renderReviewToFrame(
|
|
|
725
765
|
await renderOnce()
|
|
726
766
|
}
|
|
727
767
|
|
|
728
|
-
// Wait for
|
|
729
|
-
await
|
|
768
|
+
// Wait for tree-sitter highlighting + render stabilization
|
|
769
|
+
await waitForHighlightAndRenderStabilization(renderer, renderOnce, options.stabilizeMs ?? 2000)
|
|
730
770
|
|
|
731
771
|
// Capture the final frame
|
|
732
772
|
const buffer = renderer.currentRenderBuffer
|
|
@@ -792,7 +832,7 @@ export async function captureReviewResponsiveHtml(
|
|
|
792
832
|
// These act as upper bounds to prevent runaway memory usage
|
|
793
833
|
const desktopRows = Math.max(options.baseRows * 3, 5000)
|
|
794
834
|
const mobileRows = Math.max(Math.ceil(desktopRows * (options.desktopCols / options.mobileCols)), 10000)
|
|
795
|
-
const stabilizeMs = options.stabilizeMs ??
|
|
835
|
+
const stabilizeMs = options.stabilizeMs ?? 2000
|
|
796
836
|
|
|
797
837
|
// Generate OG image from first few hunks' raw diff (in parallel with HTML renders)
|
|
798
838
|
const ogImagePromise = options.skipOgImage
|
|
File without changes
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
diff --git a/config.json b/config.json
|
|
2
|
-
--- a/config.json
|
|
3
|
-
+++ b/config.json
|
|
4
|
-
@@ -1,9 +1,11 @@
|
|
5
|
-
{
|
|
6
|
-
- "name": "my-app",
|
|
7
|
-
+ "name": "my-awesome-app",
|
|
8
|
-
- "version": "1.0.0",
|
|
9
|
-
+ "version": "2.0.0",
|
|
10
|
-
"description": "A sample app",
|
|
11
|
-
- "main": "index.js",
|
|
12
|
-
+ "main": "dist/index.js",
|
|
13
|
-
+ "types": "dist/index.d.ts",
|
|
14
|
-
"scripts": {
|
|
15
|
-
- "build": "tsc"
|
|
16
|
-
+ "build": "tsc --project tsconfig.build.json",
|
|
17
|
-
+ "test": "bun test"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
diff --git a/src/index.ts b/src/index.ts
|
|
2
|
-
--- a/src/index.ts
|
|
3
|
-
+++ b/src/index.ts
|
|
4
|
-
@@ -1,4 +1,6 @@
|
|
5
|
-
import { App } from './app'
|
|
6
|
-
+import { Logger } from './logger'
|
|
7
|
-
|
|
8
|
-
const app = new App()
|
|
9
|
-
+const logger = new Logger()
|
|
10
|
-
app.start()
|
|
11
|
-
diff --git a/src/logger.ts b/src/logger.ts
|
|
12
|
-
new file mode 100644
|
|
13
|
-
--- /dev/null
|
|
14
|
-
+++ b/src/logger.ts
|
|
15
|
-
@@ -0,0 +1,5 @@
|
|
16
|
-
+export class Logger {
|
|
17
|
-
+ log(msg: string) {
|
|
18
|
-
+ console.log(`[LOG] ${msg}`)
|
|
19
|
-
+ }
|
|
20
|
-
+}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
diff --git a/src/utils.ts b/src/utils.ts
|
|
2
|
-
new file mode 100644
|
|
3
|
-
--- /dev/null
|
|
4
|
-
+++ b/src/utils.ts
|
|
5
|
-
@@ -0,0 +1,7 @@
|
|
6
|
-
+export function clamp(value: number, min: number, max: number): number {
|
|
7
|
-
+ return Math.min(Math.max(value, min), max)
|
|
8
|
-
+}
|
|
9
|
-
+
|
|
10
|
-
+export function identity<T>(x: T): T {
|
|
11
|
-
+ return x
|
|
12
|
-
+}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
diff --git a/src/old-name.ts b/src/new-name.ts
|
|
2
|
-
similarity index 90%
|
|
3
|
-
rename from src/old-name.ts
|
|
4
|
-
rename to src/new-name.ts
|
|
5
|
-
--- a/src/old-name.ts
|
|
6
|
-
+++ b/src/new-name.ts
|
|
7
|
-
@@ -1,3 +1,3 @@
|
|
8
|
-
-export const name = 'old'
|
|
9
|
-
+export const name = 'new'
|
|
10
|
-
export const version = 1
|
|
11
|
-
export default name
|