|
| 1 | +import fs from 'node:fs/promises'; |
| 2 | +import * as cheerio from 'cheerio'; |
| 3 | +import {outdent} from 'outdent'; |
| 4 | +import { |
| 5 | + DATA_DIRECTORY, |
| 6 | + readGlobals, |
| 7 | +} from '../utilities.mjs'; |
| 8 | +import {getDataDiff} from './utilities.mjs'; |
| 9 | + |
| 10 | +// https://tc39.es/ecma262/ |
| 11 | +const SPECIFICATION_URLS = [ |
| 12 | + 'https://raw.githubusercontent.com/tc39/ecma262/HEAD/spec.html', |
| 13 | + 'https://cdn.jsdelivr.net/gh/tc39/ecma262/spec.html', |
| 14 | +]; |
| 15 | +const CACHE_FILE = new URL('../.cache/spec.html', import.meta.url); |
| 16 | + |
| 17 | +const additionalGlobals = [ |
| 18 | + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects#internationalization |
| 19 | + // https://www.ecma-international.org/publications-and-standards/standards/ecma-402/ |
| 20 | + 'Intl', |
| 21 | + |
| 22 | + // Annex B |
| 23 | + // https://tc39.es/ecma262/#sec-additional-built-in-properties |
| 24 | + 'escape', |
| 25 | + 'unescape', |
| 26 | +]; |
| 27 | + |
| 28 | +const getText = async url => { |
| 29 | + const response = await fetch(url); |
| 30 | + const text = await response.text(); |
| 31 | + return text; |
| 32 | +}; |
| 33 | + |
| 34 | +const getSpecification = async () => { |
| 35 | + let stat; |
| 36 | + |
| 37 | + try { |
| 38 | + stat = await fs.stat(CACHE_FILE); |
| 39 | + } catch {} |
| 40 | + |
| 41 | + if (stat) { |
| 42 | + if (Date.now() - stat.ctimeMs < /* 10 hours */ 10 * 60 * 60 * 1000) { |
| 43 | + return fs.readFile(CACHE_FILE, 'utf8'); |
| 44 | + } |
| 45 | + |
| 46 | + await fs.rm(CACHE_FILE); |
| 47 | + } |
| 48 | + |
| 49 | + const text = await Promise.any(SPECIFICATION_URLS.map(url => getText(url))); |
| 50 | + |
| 51 | + await fs.mkdir(new URL('./', CACHE_FILE), {recursive: true}); |
| 52 | + await fs.writeFile(CACHE_FILE, text); |
| 53 | + |
| 54 | + return text; |
| 55 | +}; |
| 56 | + |
| 57 | +function * getGlobalObjects($) { |
| 58 | + for (const element of $('emu-clause#sec-global-object emu-clause:not([type]) > h1')) { |
| 59 | + let text = $(element).text().trim(); |
| 60 | + |
| 61 | + // Function shape `Array ( . . . )` |
| 62 | + text = text.match(/^(?<functionName>\w+?)\s*\(.*?\)$/)?.groups.functionName ?? text; |
| 63 | + |
| 64 | + if (!/^\w+$/.test(text)) { |
| 65 | + continue; |
| 66 | + } |
| 67 | + |
| 68 | + yield text; |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +async function parseSpecification() { |
| 73 | + const specification = await getSpecification(); |
| 74 | + const $ = cheerio.load(specification); |
| 75 | + const meta = $('pre.metadata').text(); |
| 76 | + const {year} = meta.match(/title: ECMAScript® (?<year>\d{4})/).groups; |
| 77 | + |
| 78 | + return { |
| 79 | + year, |
| 80 | + data: Object.fromEntries([...getGlobalObjects($), ...additionalGlobals].map(name => [name, false])), |
| 81 | + }; |
| 82 | +} |
| 83 | + |
| 84 | +async function buildYearlyBuiltinGlobals(job, options) { |
| 85 | + const {year, data} = await parseSpecification(); |
| 86 | + const environment = `es${year}`; |
| 87 | + const original = await readGlobals(environment, {ignoreNonExits: true}); |
| 88 | + const previousYear = Number(year) - 1; |
| 89 | + const previousYearData = await readGlobals(`es${previousYear}`); |
| 90 | + const previousYearProperties = new Set(Object.keys(previousYearData)); |
| 91 | + const addedEntries = Object.entries(data).filter(([name]) => !previousYearProperties.has(name)); |
| 92 | + |
| 93 | + const content = addedEntries.length === 0 |
| 94 | + ? `export {default} from './es${previousYear}.mjs';` |
| 95 | + : outdent` |
| 96 | + import {mergeGlobals} from '../utilities.mjs'; |
| 97 | + import es${previousYear}Globals from './es${previousYear}.mjs'; |
| 98 | +
|
| 99 | + export default mergeGlobals(es${previousYear}Globals, { |
| 100 | + ${addedEntries.map(([name, value]) => `\t${name}: ${value},`).join('\n')} |
| 101 | + }); |
| 102 | + `; |
| 103 | + |
| 104 | + const file = new URL(`${environment}.mjs`, DATA_DIRECTORY); |
| 105 | + |
| 106 | + const code = outdent` |
| 107 | + // This file is autogenerated by scripts |
| 108 | + // Do NOT modify this file manually |
| 109 | +
|
| 110 | + ${content}; |
| 111 | + `; |
| 112 | + |
| 113 | + if (!options.dryRun) { |
| 114 | + await fs.writeFile(file, code + '\n'); |
| 115 | + } |
| 116 | + |
| 117 | + return { |
| 118 | + environment, |
| 119 | + ...getDataDiff(original, data), |
| 120 | + }; |
| 121 | +} |
| 122 | + |
| 123 | +async function getBuiltinGlobals() { |
| 124 | + const {data} = await parseSpecification(); |
| 125 | + return data; |
| 126 | +} |
| 127 | + |
| 128 | +export {getBuiltinGlobals, buildYearlyBuiltinGlobals}; |
0 commit comments