Skip to content

Commit 529f22d

Browse files
committed
Merge branch 'release-2.22.10' into release
2 parents 3b9ceb5 + 9121567 commit 529f22d

14 files changed

Lines changed: 422 additions & 26615 deletions

File tree

‎client/images/play.svg‎

Lines changed: 2 additions & 4 deletions
Loading

‎client/images/stop.svg‎

Lines changed: 3 additions & 4 deletions
Loading

‎client/modules/About/About.styles.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export const ContactHandles = styled.p`
177177
width: 50%;
178178
179179
& a {
180-
color: ${prop('logoColor')};
180+
color: ${prop('linkTextColor')};
181181
text-decoration: underline;
182182
183183
&:hover {
@@ -204,7 +204,7 @@ export const Footer = styled.div`
204204
205205
& a {
206206
margin: ${remSize(20)} 9.5% 0 0;
207-
color: ${prop('logoColor')};
207+
color: ${prop('linkTextColor')};
208208
text-decoration: underline;
209209
210210
&:hover {

‎client/modules/IDE/components/Header/Toolbar.jsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ const Toolbar = (props) => {
3636
const showVisibilityDropdown = project?.owner && userIsOwner;
3737

3838
const playButtonClass = classNames({
39-
'toolbar__play-button': true,
40-
'toolbar__play-button--selected': isPlaying
39+
'toolbar-start-stop-button play': true,
40+
'toolbar-start-stop-button--selected play': isPlaying
4141
});
4242
const stopButtonClass = classNames({
43-
'toolbar__stop-button': true,
44-
'toolbar__stop-button--selected': !isPlaying
43+
'toolbar-start-stop-button stop': true,
44+
'toolbar-start-stop-button--selected stop': !isPlaying
4545
});
4646
const preferencesButtonClass = classNames({
4747
'toolbar__preferences-button': true,

‎client/modules/Preview/jsPreprocess.js‎

Lines changed: 20 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as acorn from 'acorn';
22
import * as walk from 'acorn-walk';
3-
import escodegen from 'escodegen';
43
import { hasNoProtect } from '../IDE/utils/loopProtection';
54

65
const LOOP_TIMEOUT_MS = 100;
@@ -31,81 +30,11 @@ function collectShaderFunctionNames(ast) {
3130
}
3231

3332
function makeVarDecl(varName) {
34-
return {
35-
type: 'VariableDeclaration',
36-
kind: 'var',
37-
declarations: [
38-
{
39-
type: 'VariableDeclarator',
40-
id: { type: 'Identifier', name: varName },
41-
init: {
42-
type: 'CallExpression',
43-
callee: {
44-
type: 'MemberExpression',
45-
object: { type: 'Identifier', name: 'Date' },
46-
property: { type: 'Identifier', name: 'now' },
47-
computed: false
48-
},
49-
arguments: []
50-
}
51-
}
52-
]
53-
};
33+
return `var ${varName} = Date.now(); `;
5434
}
5535

5636
function makeCheckStatement(varName, line) {
57-
return {
58-
type: 'IfStatement',
59-
test: {
60-
type: 'BinaryExpression',
61-
operator: '>',
62-
left: {
63-
type: 'BinaryExpression',
64-
operator: '-',
65-
left: {
66-
type: 'CallExpression',
67-
callee: {
68-
type: 'MemberExpression',
69-
object: { type: 'Identifier', name: 'Date' },
70-
property: { type: 'Identifier', name: 'now' },
71-
computed: false
72-
},
73-
arguments: []
74-
},
75-
right: { type: 'Identifier', name: varName }
76-
},
77-
right: {
78-
type: 'Literal',
79-
value: LOOP_TIMEOUT_MS,
80-
raw: String(LOOP_TIMEOUT_MS)
81-
}
82-
},
83-
consequent: {
84-
type: 'BlockStatement',
85-
body: [
86-
{
87-
type: 'ExpressionStatement',
88-
expression: {
89-
type: 'CallExpression',
90-
callee: {
91-
type: 'MemberExpression',
92-
object: {
93-
type: 'MemberExpression',
94-
object: { type: 'Identifier', name: 'window' },
95-
property: { type: 'Identifier', name: 'loopProtect' },
96-
computed: false
97-
},
98-
property: { type: 'Identifier', name: 'hit' },
99-
computed: false
100-
},
101-
arguments: [{ type: 'Literal', value: line, raw: String(line) }]
102-
}
103-
},
104-
{ type: 'BreakStatement', label: null }
105-
]
106-
},
107-
alternate: null
108-
};
37+
return `if (Date.now() - ${varName} > ${LOOP_TIMEOUT_MS}) { window.loopProtect.hit(${line}); break; }`;
10938
}
11039

11140
function collectLoopsToProtect(ast, shaderNames) {
@@ -167,26 +96,34 @@ function collectLoopsToProtect(ast, shaderNames) {
16796
return loops;
16897
}
16998

170-
function injectProtection(loops) {
99+
// Insert protection without changing the original line layout.
100+
function injectProtection(jsText, loops) {
101+
const edits = [];
102+
171103
loops.forEach(({ loop, parentBlock }, idx) => {
172104
const varName = `_LP${idx}`;
173105
const { line } = loop.loc.start;
174106
const check = makeCheckStatement(varName, line);
175107

176108
if (loop.body.type === 'BlockStatement') {
177-
loop.body.body.unshift(check);
109+
edits.push({ pos: loop.body.start + 1, rank: 1, text: ` ${check}` });
178110
} else {
179-
loop.body = { type: 'BlockStatement', body: [check, loop.body] };
111+
edits.push({ pos: loop.body.start, rank: 1, text: `{ ${check} ` });
112+
edits.push({ pos: loop.body.end, rank: 0, text: ' }' });
180113
}
181114

182-
if (parentBlock) {
183-
const varDecl = makeVarDecl(varName);
184-
const nodeIdx = parentBlock.body.indexOf(loop);
185-
if (nodeIdx !== -1) {
186-
parentBlock.body.splice(nodeIdx, 0, varDecl);
187-
}
115+
if (parentBlock && parentBlock.body.includes(loop)) {
116+
edits.push({ pos: loop.start, rank: 2, text: makeVarDecl(varName) });
188117
}
189118
});
119+
120+
// Apply edits from the end so earlier offsets remain valid.
121+
edits.sort((a, b) => b.pos - a.pos || b.rank - a.rank);
122+
123+
return edits.reduce(
124+
(result, { pos, text }) => result.slice(0, pos) + text + result.slice(pos),
125+
jsText
126+
);
190127
}
191128

192129
function parseJs(jsText) {
@@ -215,7 +152,5 @@ export function jsPreprocess(jsText, indexSrc) {
215152

216153
if (loops.length === 0) return jsText;
217154

218-
injectProtection(loops);
219-
220-
return escodegen.generate(ast);
155+
return injectProtection(jsText, loops);
221156
}

‎client/modules/Preview/jsPreprocess.unit.test.js‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as acorn from 'acorn';
12
import { jsPreprocess } from './jsPreprocess';
23

34
describe('jsPreprocess', () => {
@@ -148,4 +149,122 @@ describe('jsPreprocess', () => {
148149
expect(result).toContain('window.loopProtect.hit');
149150
});
150151
});
152+
153+
describe('line numbers', () => {
154+
const lineOf = (text, marker) =>
155+
text.split('\n').findIndex((line) => line.includes(marker)) + 1;
156+
157+
it('keeps a runtime error on the line where it was written (#4356)', () => {
158+
const code = [
159+
'function setup() {',
160+
' createCanvas(200, 200);',
161+
' background(220);',
162+
'',
163+
...Array.from({ length: 10 }, (_, i) => ` // padding ${i + 1}`),
164+
'',
165+
'',
166+
' let x = 0;',
167+
' for (let i = 0; i < 1; i++) {',
168+
' x += 1;',
169+
' }',
170+
'',
171+
'',
172+
' // deliberate error below',
173+
'',
174+
' definitelyNotAFunction();',
175+
'}'
176+
].join('\n');
177+
expect(lineOf(code, 'definitelyNotAFunction')).toBe(25);
178+
179+
const result = jsPreprocess(code, '');
180+
181+
expect(result).toContain('window.loopProtect.hit(18)');
182+
expect(lineOf(result, 'definitelyNotAFunction')).toBe(25);
183+
});
184+
185+
it('only changes the lines that contain a loop', () => {
186+
const code = `// comment
187+
let x = 0;
188+
/* block comment
189+
over two lines */
190+
191+
for (let i = 0; i < 2; i++) {
192+
// nested loop
193+
for (let j = 0; j < 2; j++) {
194+
x += i * j;
195+
}
196+
}
197+
for (let i = 0; i < 2; i++) x++;
198+
while (x > 100) x--; do { x++; } while (x < 5);
199+
if (x > 0) {
200+
while (x > 10) x--;
201+
} else {
202+
do x++; while (x < 0);
203+
}
204+
if (x < 0) for (;;) x++; else while (x < 0) x++;
205+
outer: for (let i = 0; i < 2; i++) {
206+
for (let j = 0; j < 2; j++) {
207+
if (j) continue outer;
208+
}
209+
}
210+
211+
definitelyNotAFunction();`;
212+
213+
const result = jsPreprocess(code, '');
214+
215+
expect(() =>
216+
acorn.parse(result, { ecmaVersion: 'latest' })
217+
).not.toThrow();
218+
expect(result.match(/window\.loopProtect\.hit/g)).toHaveLength(11);
219+
const codeLines = code.split('\n');
220+
const resultLines = result.split('\n');
221+
expect(resultLines).toHaveLength(codeLines.length);
222+
const changed = codeLines
223+
.map((line, i) => (line === resultLines[i] ? null : i + 1))
224+
.filter(Boolean);
225+
expect(changed).toEqual([6, 8, 12, 13, 15, 17, 19, 20, 21]);
226+
});
227+
228+
it('inserts each guard at its own loop when edits share an offset', () => {
229+
const timer = (n) => `var _LP${n} = Date.now(); `;
230+
const check = (n, line) =>
231+
`if (Date.now() - _LP${n} > 100) { window.loopProtect.hit(${line}); break; }`;
232+
const code = 'while (a) a--;while (b) {while (c) {}}\nwhile (d) d--;';
233+
234+
expect(jsPreprocess(code, '')).toBe(
235+
`${timer(0)}while (a) { ${check(0, 1)} a--; }` +
236+
`${timer(2)}while (b) { ${check(2, 1)}` +
237+
`${timer(1)}while (c) { ${check(1, 1)}}}\n` +
238+
`${timer(3)}while (d) { ${check(3, 2)} d--; }`
239+
);
240+
});
241+
242+
it('still stops infinite loops and reports their original line', () => {
243+
const run = (code) => {
244+
const hits = [];
245+
const window = { loopProtect: { hit: (line) => hits.push(line) } };
246+
// eslint-disable-next-line no-new-func
247+
const result = new Function('window', `${code}\nreturn result;`)(
248+
window
249+
);
250+
return { result, hits };
251+
};
252+
253+
const finite = jsPreprocess(
254+
`let result = 0;
255+
for (let i = 0; i < 3; i++) {
256+
for (let j = 0; j < 3; j++) result++;
257+
}
258+
do result++; while (result < 12);`,
259+
''
260+
);
261+
expect(run(finite)).toEqual({ result: 12, hits: [] });
262+
263+
const infinite = jsPreprocess(
264+
'// comment\n\nlet result = "stopped";\nwhile (true) {}',
265+
''
266+
);
267+
expect(run(infinite)).toEqual({ result: 'stopped', hits: [4] });
268+
});
269+
});
151270
});

‎client/styles/abstracts/_placeholders.scss‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,10 @@
173173
%link {
174174
@include themify() {
175175
text-decoration: none;
176-
color: getThemifyVariable('inactive-text-color');
176+
color: getThemifyVariable('link-text-color');
177177
cursor: pointer;
178178
& g, & path {
179-
fill: getThemifyVariable('inactive-text-color');
179+
fill: getThemifyVariable('link-text-color');
180180
}
181181
&:hover {
182182
text-decoration: none;

0 commit comments

Comments
 (0)