pulse-js-framework 1.4.9 → 1.5.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/cli/release.js ADDED
@@ -0,0 +1,493 @@
1
+ /**
2
+ * Pulse CLI - Release Command
3
+ *
4
+ * Handles version bumping and release automation:
5
+ * - Bump version (patch, minor, major)
6
+ * - Update CHANGELOG.md
7
+ * - Update docs changelog page
8
+ * - Update version in docs state
9
+ * - Create git commit and tag
10
+ * - Push to remote
11
+ */
12
+
13
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
14
+ import { join, dirname } from 'path';
15
+ import { fileURLToPath } from 'url';
16
+ import { execSync } from 'child_process';
17
+ import { createInterface } from 'readline';
18
+ import { log } from './logger.js';
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const root = join(__dirname, '..');
22
+
23
+ /**
24
+ * Prompt user for input
25
+ */
26
+ function prompt(question) {
27
+ const rl = createInterface({
28
+ input: process.stdin,
29
+ output: process.stdout
30
+ });
31
+
32
+ return new Promise((resolve) => {
33
+ rl.question(question, (answer) => {
34
+ rl.close();
35
+ resolve(answer);
36
+ });
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Prompt for multiline input
42
+ */
43
+ async function promptMultiline(question) {
44
+ log.info(question);
45
+ log.info('(Enter each item on a new line, empty line to finish)');
46
+
47
+ const rl = createInterface({
48
+ input: process.stdin,
49
+ output: process.stdout
50
+ });
51
+
52
+ const lines = [];
53
+
54
+ return new Promise((resolve) => {
55
+ rl.on('line', (line) => {
56
+ if (line.trim() === '') {
57
+ rl.close();
58
+ resolve(lines);
59
+ } else {
60
+ lines.push(line.trim());
61
+ }
62
+ });
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Parse version string
68
+ */
69
+ function parseVersion(version) {
70
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
71
+ if (!match) throw new Error(`Invalid version format: ${version}`);
72
+ return {
73
+ major: parseInt(match[1], 10),
74
+ minor: parseInt(match[2], 10),
75
+ patch: parseInt(match[3], 10)
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Bump version based on type
81
+ */
82
+ function bumpVersion(version, type) {
83
+ const v = parseVersion(version);
84
+ switch (type) {
85
+ case 'major':
86
+ return `${v.major + 1}.0.0`;
87
+ case 'minor':
88
+ return `${v.major}.${v.minor + 1}.0`;
89
+ case 'patch':
90
+ return `${v.major}.${v.minor}.${v.patch + 1}`;
91
+ default:
92
+ throw new Error(`Unknown version type: ${type}`);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get current date in YYYY-MM-DD format
98
+ */
99
+ function getCurrentDate() {
100
+ const now = new Date();
101
+ return now.toISOString().split('T')[0];
102
+ }
103
+
104
+ /**
105
+ * Get current month and year
106
+ */
107
+ function getCurrentMonthYear() {
108
+ const months = [
109
+ 'January', 'February', 'March', 'April', 'May', 'June',
110
+ 'July', 'August', 'September', 'October', 'November', 'December'
111
+ ];
112
+ const now = new Date();
113
+ return `${months[now.getMonth()]} ${now.getFullYear()}`;
114
+ }
115
+
116
+ /**
117
+ * Update package.json version
118
+ */
119
+ function updatePackageJson(newVersion) {
120
+ const pkgPath = join(root, 'package.json');
121
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
122
+ pkg.version = newVersion;
123
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
124
+ log.info(` Updated package.json to v${newVersion}`);
125
+ }
126
+
127
+ /**
128
+ * Update docs/src/state.js version
129
+ */
130
+ function updateDocsState(newVersion) {
131
+ const statePath = join(root, 'docs/src/state.js');
132
+ if (!existsSync(statePath)) {
133
+ log.warn(' docs/src/state.js not found, skipping');
134
+ return;
135
+ }
136
+
137
+ let content = readFileSync(statePath, 'utf-8');
138
+ content = content.replace(
139
+ /export const version = '[^']+'/,
140
+ `export const version = '${newVersion}'`
141
+ );
142
+ writeFileSync(statePath, content);
143
+ log.info(` Updated docs/src/state.js to v${newVersion}`);
144
+ }
145
+
146
+ /**
147
+ * Update CHANGELOG.md
148
+ */
149
+ function updateChangelog(newVersion, title, changes) {
150
+ const changelogPath = join(root, 'CHANGELOG.md');
151
+ if (!existsSync(changelogPath)) {
152
+ log.warn(' CHANGELOG.md not found, skipping');
153
+ return;
154
+ }
155
+
156
+ let content = readFileSync(changelogPath, 'utf-8');
157
+
158
+ // Build changelog entry
159
+ const date = getCurrentDate();
160
+ let entry = `## [${newVersion}] - ${date}\n\n`;
161
+
162
+ if (title) {
163
+ entry += `### ${title}\n\n`;
164
+ }
165
+
166
+ if (changes.added && changes.added.length > 0) {
167
+ entry += `### Added\n\n`;
168
+ for (const item of changes.added) {
169
+ entry += `- ${item}\n`;
170
+ }
171
+ entry += '\n';
172
+ }
173
+
174
+ if (changes.changed && changes.changed.length > 0) {
175
+ entry += `### Changed\n\n`;
176
+ for (const item of changes.changed) {
177
+ entry += `- ${item}\n`;
178
+ }
179
+ entry += '\n';
180
+ }
181
+
182
+ if (changes.fixed && changes.fixed.length > 0) {
183
+ entry += `### Fixed\n\n`;
184
+ for (const item of changes.fixed) {
185
+ entry += `- ${item}\n`;
186
+ }
187
+ entry += '\n';
188
+ }
189
+
190
+ if (changes.removed && changes.removed.length > 0) {
191
+ entry += `### Removed\n\n`;
192
+ for (const item of changes.removed) {
193
+ entry += `- ${item}\n`;
194
+ }
195
+ entry += '\n';
196
+ }
197
+
198
+ // Insert after the header section (after line 6)
199
+ const lines = content.split('\n');
200
+ const insertIndex = lines.findIndex(line => line.startsWith('## ['));
201
+
202
+ if (insertIndex !== -1) {
203
+ lines.splice(insertIndex, 0, entry);
204
+ } else {
205
+ // No existing version entries, add after header
206
+ lines.push(entry);
207
+ }
208
+
209
+ writeFileSync(changelogPath, lines.join('\n'));
210
+ log.info(` Updated CHANGELOG.md with v${newVersion}`);
211
+ }
212
+
213
+ /**
214
+ * Update docs changelog page
215
+ */
216
+ function updateDocsChangelog(newVersion, title, changes) {
217
+ const changelogPagePath = join(root, 'docs/src/pages/ChangelogPage.js');
218
+ if (!existsSync(changelogPagePath)) {
219
+ log.warn(' docs/src/pages/ChangelogPage.js not found, skipping');
220
+ return;
221
+ }
222
+
223
+ let content = readFileSync(changelogPagePath, 'utf-8');
224
+ const monthYear = getCurrentMonthYear();
225
+
226
+ // Build HTML changelog section
227
+ let section = `
228
+ <section class="doc-section changelog-section">
229
+ <h2>v${newVersion} - ${title || 'Release'}</h2>
230
+ <p class="release-date">${monthYear}</p>
231
+
232
+ <div class="changelog-group">`;
233
+
234
+ // Combine all changes into feature list
235
+ const allChanges = [
236
+ ...(changes.added || []).map(c => `<strong>Added:</strong> ${escapeHtml(c)}`),
237
+ ...(changes.changed || []).map(c => `<strong>Changed:</strong> ${escapeHtml(c)}`),
238
+ ...(changes.fixed || []).map(c => `<strong>Fixed:</strong> ${escapeHtml(c)}`),
239
+ ...(changes.removed || []).map(c => `<strong>Removed:</strong> ${escapeHtml(c)}`)
240
+ ];
241
+
242
+ if (allChanges.length > 0) {
243
+ section += `
244
+ <ul class="feature-list">`;
245
+ for (const change of allChanges) {
246
+ section += `
247
+ <li>${change}</li>`;
248
+ }
249
+ section += `
250
+ </ul>`;
251
+ }
252
+
253
+ section += `
254
+ </div>
255
+ </section>
256
+ `;
257
+
258
+ // Find where to insert (after the intro paragraph, before first section)
259
+ const insertMarker = '<section class="doc-section changelog-section">';
260
+ const insertIndex = content.indexOf(insertMarker);
261
+
262
+ if (insertIndex !== -1) {
263
+ content = content.slice(0, insertIndex) + section + content.slice(insertIndex);
264
+ }
265
+
266
+ writeFileSync(changelogPagePath, content);
267
+ log.info(` Updated docs/src/pages/ChangelogPage.js with v${newVersion}`);
268
+ }
269
+
270
+ /**
271
+ * Escape HTML entities
272
+ */
273
+ function escapeHtml(str) {
274
+ return str
275
+ .replace(/&/g, '&amp;')
276
+ .replace(/</g, '&lt;')
277
+ .replace(/>/g, '&gt;')
278
+ .replace(/"/g, '&quot;');
279
+ }
280
+
281
+ /**
282
+ * Update CLAUDE.md if needed
283
+ */
284
+ function updateClaudeMd(newVersion) {
285
+ // CLAUDE.md reads version from package.json, so no update needed
286
+ log.info(' CLAUDE.md reads version from package.json (no update needed)');
287
+ }
288
+
289
+ /**
290
+ * Update README.md if needed
291
+ */
292
+ function updateReadme(newVersion) {
293
+ // README.md doesn't have a version number to update
294
+ // But we could update version-specific feature mentions if needed
295
+ log.info(' README.md has no version-specific content to update');
296
+ }
297
+
298
+ /**
299
+ * Execute git commands
300
+ */
301
+ function gitCommitTagPush(newVersion, dryRun = false) {
302
+ const commands = [
303
+ 'git add -A',
304
+ `git commit -m "$(cat <<'EOF'\nv${newVersion}\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)"`,
305
+ `git tag -a v${newVersion} -m "Release v${newVersion}"`,
306
+ 'git push',
307
+ 'git push --tags'
308
+ ];
309
+
310
+ for (const cmd of commands) {
311
+ if (dryRun) {
312
+ log.info(` [dry-run] ${cmd}`);
313
+ } else {
314
+ log.info(` Running: ${cmd.split('\n')[0]}...`);
315
+ try {
316
+ execSync(cmd, { cwd: root, stdio: 'inherit', shell: '/bin/bash' });
317
+ } catch (error) {
318
+ log.error(` Failed: ${error.message}`);
319
+ throw error;
320
+ }
321
+ }
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Show usage
327
+ */
328
+ function showUsage() {
329
+ log.info(`
330
+ Usage: pulse release <type> [options]
331
+
332
+ Types:
333
+ patch Bump patch version (1.0.0 -> 1.0.1)
334
+ minor Bump minor version (1.0.0 -> 1.1.0)
335
+ major Bump major version (1.0.0 -> 2.0.0)
336
+
337
+ Options:
338
+ --dry-run Show what would be done without making changes
339
+ --no-push Create commit and tag but don't push
340
+ --title <text> Release title (e.g., "Performance Improvements")
341
+ --skip-prompt Use empty changelog (for automated releases)
342
+
343
+ Examples:
344
+ pulse release patch
345
+ pulse release minor --title "New Features"
346
+ pulse release major --dry-run
347
+ `);
348
+ }
349
+
350
+ /**
351
+ * Main release command
352
+ */
353
+ export async function runRelease(args) {
354
+ const type = args[0];
355
+
356
+ if (!type || !['patch', 'minor', 'major'].includes(type)) {
357
+ showUsage();
358
+ process.exit(1);
359
+ }
360
+
361
+ // Parse options
362
+ const dryRun = args.includes('--dry-run');
363
+ const noPush = args.includes('--no-push');
364
+ const skipPrompt = args.includes('--skip-prompt');
365
+
366
+ let title = '';
367
+ const titleIndex = args.indexOf('--title');
368
+ if (titleIndex !== -1 && args[titleIndex + 1]) {
369
+ title = args[titleIndex + 1];
370
+ }
371
+
372
+ // Read current version
373
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8'));
374
+ const currentVersion = pkg.version;
375
+ const newVersion = bumpVersion(currentVersion, type);
376
+
377
+ log.info('');
378
+ log.info(`Pulse Release: v${currentVersion} -> v${newVersion}`);
379
+ log.info('='.repeat(50));
380
+
381
+ if (dryRun) {
382
+ log.warn('DRY RUN - No changes will be made');
383
+ log.info('');
384
+ }
385
+
386
+ // Check for uncommitted changes
387
+ try {
388
+ const status = execSync('git status --porcelain', { cwd: root, encoding: 'utf-8' });
389
+ if (status.trim()) {
390
+ log.warn('You have uncommitted changes:');
391
+ log.info(status);
392
+ const proceed = await prompt('Continue anyway? (y/N) ');
393
+ if (proceed.toLowerCase() !== 'y') {
394
+ log.info('Aborted.');
395
+ process.exit(0);
396
+ }
397
+ }
398
+ } catch (error) {
399
+ log.error('Failed to check git status');
400
+ process.exit(1);
401
+ }
402
+
403
+ // Collect changelog entries
404
+ let changes = { added: [], changed: [], fixed: [], removed: [] };
405
+
406
+ if (!skipPrompt) {
407
+ if (!title) {
408
+ title = await prompt('Release title (e.g., "Performance Improvements"): ');
409
+ }
410
+
411
+ log.info('');
412
+ log.info('Enter changelog items (leave empty to skip category):');
413
+ log.info('');
414
+
415
+ changes.added = await promptMultiline('Added (new features):');
416
+ changes.changed = await promptMultiline('Changed (modifications):');
417
+ changes.fixed = await promptMultiline('Fixed (bug fixes):');
418
+ changes.removed = await promptMultiline('Removed (deprecated features):');
419
+ }
420
+
421
+ const hasChanges = Object.values(changes).some(arr => arr.length > 0);
422
+
423
+ if (!hasChanges && !skipPrompt) {
424
+ const proceed = await prompt('No changelog entries. Continue? (y/N) ');
425
+ if (proceed.toLowerCase() !== 'y') {
426
+ log.info('Aborted.');
427
+ process.exit(0);
428
+ }
429
+ }
430
+
431
+ log.info('');
432
+ log.info('Updating files...');
433
+
434
+ if (!dryRun) {
435
+ // 1. Update package.json
436
+ updatePackageJson(newVersion);
437
+
438
+ // 2. Update docs state
439
+ updateDocsState(newVersion);
440
+
441
+ // 3. Update CHANGELOG.md
442
+ if (hasChanges) {
443
+ updateChangelog(newVersion, title, changes);
444
+ }
445
+
446
+ // 4. Update docs changelog page
447
+ if (hasChanges) {
448
+ updateDocsChangelog(newVersion, title, changes);
449
+ }
450
+
451
+ // 5. CLAUDE.md and README.md
452
+ updateClaudeMd(newVersion);
453
+ updateReadme(newVersion);
454
+ } else {
455
+ log.info(' [dry-run] Would update package.json');
456
+ log.info(' [dry-run] Would update docs/src/state.js');
457
+ if (hasChanges) {
458
+ log.info(' [dry-run] Would update CHANGELOG.md');
459
+ log.info(' [dry-run] Would update docs/src/pages/ChangelogPage.js');
460
+ }
461
+ }
462
+
463
+ log.info('');
464
+ log.info('Git operations...');
465
+
466
+ if (!dryRun) {
467
+ if (noPush) {
468
+ // Only commit and tag, no push
469
+ execSync('git add -A', { cwd: root, stdio: 'inherit' });
470
+ execSync(`git commit -m "v${newVersion}\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"`, {
471
+ cwd: root,
472
+ stdio: 'inherit',
473
+ shell: '/bin/bash'
474
+ });
475
+ execSync(`git tag -a v${newVersion} -m "Release v${newVersion}"`, { cwd: root, stdio: 'inherit' });
476
+ log.info(' Created commit and tag (--no-push specified)');
477
+ } else {
478
+ gitCommitTagPush(newVersion, false);
479
+ }
480
+ } else {
481
+ gitCommitTagPush(newVersion, true);
482
+ }
483
+
484
+ log.info('');
485
+ log.info(`Release v${newVersion} complete!`);
486
+ log.info('');
487
+
488
+ if (!dryRun && !noPush) {
489
+ log.info('Next steps:');
490
+ log.info(` 1. Create GitHub release: https://github.com/vincenthirtz/pulse-js-framework/releases/new?tag=v${newVersion}`);
491
+ log.info(' 2. Publish to npm: npm publish');
492
+ }
493
+ }
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { TokenType, tokenize } from './lexer.js';
8
+ import { ParserError, SUGGESTIONS } from '../core/errors.js';
8
9
 
9
10
  // AST Node types
10
11
  export const NodeType = {
@@ -122,9 +123,10 @@ export class Parser {
122
123
  expect(type, message = null) {
123
124
  if (!this.is(type)) {
124
125
  const token = this.current();
125
- throw new Error(
126
- message ||
127
- `Expected ${type} but got ${token?.type} at line ${token?.line}:${token?.column}`
126
+ throw this.createError(
127
+ message || `Expected ${type} but got ${token?.type}`,
128
+ token,
129
+ { suggestion: SUGGESTIONS['unexpected-token']?.(type, token?.type) }
128
130
  );
129
131
  }
130
132
  return this.advance();
@@ -132,14 +134,19 @@ export class Parser {
132
134
 
133
135
  /**
134
136
  * Create a parse error with detailed information
137
+ * @param {string} message - Error message
138
+ * @param {Object} [token] - Token where error occurred
139
+ * @param {Object} [options] - Additional options (suggestion, code)
140
+ * @returns {ParserError} The parser error
135
141
  */
136
- createError(message, token = null) {
142
+ createError(message, token = null, options = {}) {
137
143
  const t = token || this.current();
138
- const error = new Error(message);
139
- error.line = t?.line || 1;
140
- error.column = t?.column || 1;
141
- error.token = t;
142
- return error;
144
+ return new ParserError(message, {
145
+ line: t?.line || 1,
146
+ column: t?.column || 1,
147
+ token: t,
148
+ ...options
149
+ });
143
150
  }
144
151
 
145
152
  /**
@@ -180,49 +187,63 @@ export class Parser {
180
187
  // Props block
181
188
  else if (this.is(TokenType.PROPS)) {
182
189
  if (program.props) {
183
- throw this.createError('Duplicate props block - only one props block allowed per file');
190
+ throw this.createError('Duplicate props block - only one props block allowed per file', null, {
191
+ suggestion: SUGGESTIONS['duplicate-declaration']?.('props')
192
+ });
184
193
  }
185
194
  program.props = this.parsePropsBlock();
186
195
  }
187
196
  // State block
188
197
  else if (this.is(TokenType.STATE)) {
189
198
  if (program.state) {
190
- throw this.createError('Duplicate state block - only one state block allowed per file');
199
+ throw this.createError('Duplicate state block - only one state block allowed per file', null, {
200
+ suggestion: SUGGESTIONS['duplicate-declaration']?.('state')
201
+ });
191
202
  }
192
203
  program.state = this.parseStateBlock();
193
204
  }
194
205
  // View block
195
206
  else if (this.is(TokenType.VIEW)) {
196
207
  if (program.view) {
197
- throw this.createError('Duplicate view block - only one view block allowed per file');
208
+ throw this.createError('Duplicate view block - only one view block allowed per file', null, {
209
+ suggestion: SUGGESTIONS['duplicate-declaration']?.('view')
210
+ });
198
211
  }
199
212
  program.view = this.parseViewBlock();
200
213
  }
201
214
  // Actions block
202
215
  else if (this.is(TokenType.ACTIONS)) {
203
216
  if (program.actions) {
204
- throw this.createError('Duplicate actions block - only one actions block allowed per file');
217
+ throw this.createError('Duplicate actions block - only one actions block allowed per file', null, {
218
+ suggestion: SUGGESTIONS['duplicate-declaration']?.('actions')
219
+ });
205
220
  }
206
221
  program.actions = this.parseActionsBlock();
207
222
  }
208
223
  // Style block
209
224
  else if (this.is(TokenType.STYLE)) {
210
225
  if (program.style) {
211
- throw this.createError('Duplicate style block - only one style block allowed per file');
226
+ throw this.createError('Duplicate style block - only one style block allowed per file', null, {
227
+ suggestion: SUGGESTIONS['duplicate-declaration']?.('style')
228
+ });
212
229
  }
213
230
  program.style = this.parseStyleBlock();
214
231
  }
215
232
  // Router block
216
233
  else if (this.is(TokenType.ROUTER)) {
217
234
  if (program.router) {
218
- throw this.createError('Duplicate router block - only one router block allowed per file');
235
+ throw this.createError('Duplicate router block - only one router block allowed per file', null, {
236
+ suggestion: SUGGESTIONS['duplicate-declaration']?.('router')
237
+ });
219
238
  }
220
239
  program.router = this.parseRouterBlock();
221
240
  }
222
241
  // Store block
223
242
  else if (this.is(TokenType.STORE)) {
224
243
  if (program.store) {
225
- throw this.createError('Duplicate store block - only one store block allowed per file');
244
+ throw this.createError('Duplicate store block - only one store block allowed per file', null, {
245
+ suggestion: SUGGESTIONS['duplicate-declaration']?.('store')
246
+ });
226
247
  }
227
248
  program.store = this.parseStoreBlock();
228
249
  }
@@ -415,8 +436,8 @@ export class Parser {
415
436
 
416
437
  if (this.is(TokenType.IDENT)) return this.parseIdentifierOrExpression();
417
438
 
418
- throw new Error(
419
- `Unexpected token ${this.current()?.type} in value at line ${this.current()?.line}`
439
+ throw this.createError(
440
+ `Unexpected token ${this.current()?.type} in value`
420
441
  );
421
442
  }
422
443
 
@@ -1000,8 +1021,8 @@ export class Parser {
1000
1021
  return this.parseIdentifierOrExpression();
1001
1022
  }
1002
1023
 
1003
- throw new Error(
1004
- `Unexpected token ${this.current()?.type} in expression at line ${this.current()?.line}`
1024
+ throw this.createError(
1025
+ `Unexpected token ${this.current()?.type} in expression`
1005
1026
  );
1006
1027
  }
1007
1028
 
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Transformer Constants
3
+ * Shared constants for the Pulse transformer modules
4
+ * @module pulse-js-framework/compiler/transformer/constants
5
+ */
6
+
7
+ /** Generate a unique scope ID for CSS scoping */
8
+ export const generateScopeId = () => 'p' + Math.random().toString(36).substring(2, 8);
9
+
10
+ /** Token types that should not have space after them */
11
+ export const NO_SPACE_AFTER = new Set([
12
+ 'DOT', 'LPAREN', 'LBRACKET', 'LBRACE', 'NOT', 'SPREAD',
13
+ '.', '(', '[', '{', '!', '~', '...'
14
+ ]);
15
+
16
+ /** Token types that should not have space before them */
17
+ export const NO_SPACE_BEFORE = new Set([
18
+ 'DOT', 'RPAREN', 'RBRACKET', 'RBRACE', 'SEMICOLON', 'COMMA',
19
+ 'INCREMENT', 'DECREMENT', 'LPAREN', 'LBRACKET',
20
+ '.', ')', ']', '}', ';', ',', '++', '--', '(', '['
21
+ ]);
22
+
23
+ /** Punctuation that should not have space before */
24
+ export const PUNCT_NO_SPACE_BEFORE = [
25
+ 'DOT', 'LPAREN', 'RPAREN', 'LBRACKET', 'RBRACKET', 'SEMICOLON', 'COMMA', 'COLON'
26
+ ];
27
+
28
+ /** Punctuation that should not have space after */
29
+ export const PUNCT_NO_SPACE_AFTER = [
30
+ 'DOT', 'LPAREN', 'LBRACKET', 'NOT', 'COLON'
31
+ ];
32
+
33
+ /** JavaScript statement keywords */
34
+ export const STATEMENT_KEYWORDS = new Set([
35
+ 'let', 'const', 'var', 'return', 'if', 'else', 'for', 'while',
36
+ 'switch', 'throw', 'try', 'catch', 'finally'
37
+ ]);
38
+
39
+ /** Built-in JavaScript functions and objects */
40
+ export const BUILTIN_FUNCTIONS = new Set([
41
+ 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
42
+ 'alert', 'confirm', 'prompt', 'console', 'document', 'window',
43
+ 'Math', 'JSON', 'Date', 'Array', 'Object', 'String', 'Number',
44
+ 'Boolean', 'Promise', 'fetch'
45
+ ]);
46
+
47
+ /** Token types that start statements */
48
+ export const STATEMENT_TOKEN_TYPES = new Set(['IF', 'FOR', 'EACH']);
49
+
50
+ /** Token types that end statements */
51
+ export const STATEMENT_END_TYPES = new Set([
52
+ 'RBRACE', 'RPAREN', 'RBRACKET', 'SEMICOLON', 'STRING',
53
+ 'NUMBER', 'TRUE', 'FALSE', 'NULL', 'IDENT'
54
+ ]);