diff --git a/_includes/header.html b/_includes/header.html index b8969025..85142076 100644 --- a/_includes/header.html +++ b/_includes/header.html @@ -1,5 +1,5 @@ - + @@ -13,7 +13,7 @@
- +
diff --git a/css/custom.css b/css/custom.css index fefb1846..64ef07bd 100644 --- a/css/custom.css +++ b/css/custom.css @@ -14,7 +14,7 @@ body { } a { - color: #6797e2; + color: #4772b5; text-decoration: none; } @@ -1880,7 +1880,8 @@ dl dd { } table { - display: block; + /* https://bugzilla.mozilla.org/show_bug.cgi?id=1005271 */ + /* display: block; */ width: 100%; overflow: auto; } @@ -1890,7 +1891,7 @@ table th { } table th, table td { - padding: 6px 13px; + padding: 6px 11px; border: 1px solid #ddd; } @@ -1957,9 +1958,15 @@ pre code::before, pre code::after { content: normal; } +#feature-support-scrollbox { + overflow: auto; + margin: 0 0 16px 0; +} + #feature-support { white-space: nowrap; cursor: default; + margin: 0; } #feature-support>caption { @@ -1984,7 +1991,119 @@ pre code::before, pre code::after { height: 32px; } -#feature-support td[title] { - text-decoration: underline dashed; - text-underline-position: under; +#feature-support td { + position: relative; /* for tooltip */ +} + +#feature-support td:hover, #feature-support td:focus, #feature-support td:focus-within { + background: rgba(0, 0, 0, .04); +} + +.feature-cell { + position: relative; + height: 24px; /* height of the icon inside */ + line-height: 24px; +} + +.feature-cell > sup { + font-size: .7em; + position: absolute; + top: .2em; +} + +.feature-cell > svg { + width: 24px; + height: 24px; +} + +.feature-cell.icon-yes { + color: #1B5E20; +} + +.feature-cell.icon-yes > svg .svg-stroke { + fill: #1B5E20; +} + +.feature-cell.icon-no { + color: #a96e8e; +} + +.feature-cell.icon-no > svg .svg-stroke { + fill: #a96e8e; +} + +.feature-cell.icon-flag { + color: #575581; +} + +.feature-cell.icon-flag > svg .svg-stroke { + fill: #575581; } + +.feature-cell.icon-na { + color: #78909C; +} + +.feature-cell.icon-unknown > svg .svg-stroke { + fill: #78909C; +} + +#feature-support-scrollbox + ol { + list-style: lower-alpha; + font-size: 0.7em; + margin: 0 0 1em 0; +} + +#feature-support-scrollbox + ol > li { + transition: background-color .08s ease-in-out; +} + +#feature-support-scrollbox + ol .ref-highlight { + background: #ECEFF1; +} + +.feature-tooltip { + text-align: left; + text-align: start; + white-space: normal; + background: #fefefe; + font-size: 0.8em; + border-radius: 2px; + outline: none; + + top: 0; + left: 0; + z-index: 1; + max-width: 16em; + width: max-content; + height: max-content; + padding: 12px; +} + +/* Only apply transition after the initial position was set */ +.feature-tooltip[data-placement] { + transition: transform .2s ease-in-out; +} + +.feature-tooltip, .feature-tooltip-arrow { + position: absolute; + contain: layout style; + --shadow-size: 3px; + box-shadow: 0 0 var(--shadow-size) rgba(0, 0, 0, .30); +} + +.feature-tooltip-arrow { + --arrow-size: 8px; + background: inherit; + width: var(--arrow-size); + height: var(--arrow-size); + + --c0: calc(var(--shadow-size) * -1); + --c1: calc(100% + var(--shadow-size)); + clip-path: polygon(var(--c0) var(--c1), var(--c0) var(--c0), var(--c1) var(--c0)); +} + +[data-placement="top"] > .feature-tooltip-arrow { bottom: 0; transform: translateY(50%) rotate(-135deg); } +[data-placement="bottom"] > .feature-tooltip-arrow { top: 0; transform: translateY(-50%) rotate(45deg); } +[data-placement="left"] > .feature-tooltip-arrow { right: 0; transform: translateX(50%) rotate(135deg); } +[data-placement="right"] > .feature-tooltip-arrow { left: 0; transform: translateX(-50%) rotate(-45deg); } diff --git a/features.json b/features.json index f8768ffb..b3f2a991 100644 --- a/features.json +++ b/features.json @@ -90,68 +90,65 @@ "Chrome": { "url": "https://www.google.com/chrome/", "logo": "/images/chrome.svg", - "version": "96", "features": { - "bigInt": true, - "bulkMemory": true, - "exceptions": true, - "multiValue": true, - "mutableGlobals": true, - "referenceTypes": true, - "saturatedFloatToInt": true, - "signExtensions": true, - "simd": true, - "tailCall": "#enable-experimental-webassembly-features", - "threads": true + "bigInt": "85", + "bulkMemory": "75", + "exceptions": "95", + "multiValue": "85", + "mutableGlobals": "74", + "referenceTypes": "96", + "saturatedFloatToInt": "75", + "signExtensions": "74", + "simd": "91", + "tailCall": ["flag", "Requires flag `#enable-experimental-webassembly-features`"], + "threads": "74" } }, "Firefox": { "url": "https://www.mozilla.org/firefox/", "logo": "/images/firefox.svg", - "version": "90", "features": { - "bigInt": true, - "bulkMemory": true, - "exceptions": true, - "extendedConstantExprs": "Enabled in Nightly, unavailable in Beta/Release", - "memory64": "Enabled in Nightly, unavailable in Beta/Release", - "multiValue": true, - "mutableGlobals": true, - "referenceTypes": true, - "relaxedSimd": "Enabled in Nightly, unavailable in Beta/Release", - "saturatedFloatToInt": true, - "signExtensions": true, - "simd": true, - "threads": true, - "typeReflection": "Enabled in Nightly, unavailable in Beta/Release" + "bigInt": "78", + "bulkMemory": "79", + "exceptions": "100", + "extendedConstantExprs": ["flag", "Enabled in Nightly, unavailable in Beta/Release"], + "memory64": ["flag", "Enabled in Nightly, unavailable in Beta/Release"], + "multiValue": "78", + "mutableGlobals": "61", + "referenceTypes": "79", + "relaxedSimd": ["flag", "Enabled in Nightly, unavailable in Beta/Release"], + "saturatedFloatToInt": "64", + "signExtensions": "62", + "simd": "89", + "threads": "79", + "typeReflection": ["flag", "Enabled in Nightly, unavailable in Beta/Release"] } }, "Safari": { "url": "https://www.apple.com/safari/", - "logo": "/images/safari_48x48.png", - "version": "15.2", + "logo": "/images/safari.svg", "features": { - "bigInt": true, - "bulkMemory": true, - "exceptions": true, + "bigInt": ["14.1", "Supported in desktop Safari since 14.1 and iOS Safari since 14.5"], + "bulkMemory": "15", + "exceptions": "15.2", "multiValue": true, "mutableGlobals": true, - "referenceTypes": true, - "saturatedFloatToInt": true, - "signExtensions": true, - "threads": true + "referenceTypes": "15", + "saturatedFloatToInt": "15", + "signExtensions": ["14.1", "Supported in desktop Safari since 14.1 and iOS Safari since 14.5"], + "threads": ["14.1", "Supported in desktop Safari since 14.1 and iOS Safari since 14.5"] } }, "Wasmtime": { "url": "https://wasmtime.dev/", - "logo": "/images/bca.png", + "logo": "/images/bca.svg", "version": "0.33", "features": { "bigInt": null, "bulkMemory": true, - "memory64": "--enable-memory64", - "multiMemory": "--enable-multi-memory", - "moduleLinking": "--enable-module-linking", + "memory64": ["flag", "Requires flag `--enable-memory64`"], + "multiMemory": ["flag", "Requires flag `--enable-multi-memory`"], + "moduleLinking": ["flag", "Requires flag `--enable-module-linking`"], "multiValue": true, "mutableGlobals": true, "referenceTypes": true, @@ -162,8 +159,7 @@ }, "Wasmer": { "url": "https://wasmer.io/", - "logo": "/images/wasmer.png", - "version": "2.0", + "logo": "/images/wasmer.svg", "features": { "bigInt": null, "bulkMemory": true, @@ -173,31 +169,29 @@ "saturatedFloatToInt": true, "signExtensions": true, "simd": true, - "threads": "--enable-threads" + "threads": ["flag", "Requires flag `--enable-threads`"] } }, "Node.js": { "url": "https://nodejs.org/", "logo": "/images/nodejs.svg", - "version": "16.4", "features": { "bigInt": true, "bulkMemory": true, - "exceptions": "--experimental-wasm-eh", + "exceptions": ["flag", "Requires flag `--experimental-wasm-eh`"], "multiValue": true, "mutableGlobals": true, - "referenceTypes": "--experimental-wasm-reftypes", + "referenceTypes": ["flag", "Requires flag `--experimental-wasm-reftypes`"], "saturatedFloatToInt": true, "signExtensions": true, "simd": true, - "tailCall": "--experimental-wasm-return-call", + "tailCall": ["flag", "Requires flag `--experimental-wasm-return-call`"], "threads": true } }, "Deno": { "url": "https://deno.land/", "logo": "/images/deno.svg", - "version": "1.21.3", "features": { "bigInt": true, "bulkMemory": true, diff --git a/images/bca.png b/images/bca.png deleted file mode 100644 index 9baadb36..00000000 Binary files a/images/bca.png and /dev/null differ diff --git a/images/bca.svg b/images/bca.svg new file mode 100644 index 00000000..d91371ef --- /dev/null +++ b/images/bca.svg @@ -0,0 +1 @@ +Bytecode Alliance logo \ No newline at end of file diff --git a/images/chrome.svg b/images/chrome.svg index 8d2d942e..381be82c 100644 --- a/images/chrome.svg +++ b/images/chrome.svg @@ -1 +1 @@ - \ No newline at end of file +Chrome logo \ No newline at end of file diff --git a/images/edge.svg b/images/edge.svg index e3707609..ad41c550 100644 --- a/images/edge.svg +++ b/images/edge.svg @@ -1 +1 @@ - \ No newline at end of file +Edge logo \ No newline at end of file diff --git a/images/firefox.svg b/images/firefox.svg index 951b6d2d..c530ab22 100644 --- a/images/firefox.svg +++ b/images/firefox.svg @@ -1 +1 @@ - \ No newline at end of file +Firefox logo \ No newline at end of file diff --git a/images/nodejs.svg b/images/nodejs.svg index 974a2b3e..db19b09b 100644 --- a/images/nodejs.svg +++ b/images/nodejs.svg @@ -1 +1 @@ - \ No newline at end of file +Node.js Logo \ No newline at end of file diff --git a/images/safari.svg b/images/safari.svg new file mode 100644 index 00000000..9c40a6f6 --- /dev/null +++ b/images/safari.svg @@ -0,0 +1 @@ +Safari logo \ No newline at end of file diff --git a/images/safari_144x144.png b/images/safari_144x144.png deleted file mode 100644 index a91a1e1b..00000000 Binary files a/images/safari_144x144.png and /dev/null differ diff --git a/images/safari_48x48.png b/images/safari_48x48.png deleted file mode 100644 index bcac3d70..00000000 Binary files a/images/safari_48x48.png and /dev/null differ diff --git a/images/safari_96x96.png b/images/safari_96x96.png deleted file mode 100644 index 3034690f..00000000 Binary files a/images/safari_96x96.png and /dev/null differ diff --git a/images/wasmer.png b/images/wasmer.png deleted file mode 100644 index 3b136d1b..00000000 Binary files a/images/wasmer.png and /dev/null differ diff --git a/images/wasmer.svg b/images/wasmer.svg new file mode 100644 index 00000000..c38c773f --- /dev/null +++ b/images/wasmer.svg @@ -0,0 +1 @@ +Wasmer logo \ No newline at end of file diff --git a/roadmap.js b/roadmap.js new file mode 100644 index 00000000..951a8651 --- /dev/null +++ b/roadmap.js @@ -0,0 +1,389 @@ +(async () => { + 'use strict'; + + function partitionArray(arr, condition) { + const matched = []; + const unmatched = []; + + for (const item of arr) { + if (condition(item)) { + matched.push(item); + } else { + unmatched.push(item); + } + } + + return { matched, unmatched }; + } + + function h(name, props = {}, children = []) { + const node = Object.assign(document.createElement(name), props); + node.append(...children); + return node; + } + + // Convert number to lowercase hexavigesimal like "a, b, c, .., x, y, z, aa, ab, ..", starting from zero. + // This is the same format as CSS `list-style: lower-alpha`, which is used for our footnote lists. + function toAlphabet(num) { + const digit = num % 26, + char = String.fromCharCode(97 + digit), + rem = num - digit; + return rem ? toAlphabet(Math.floor(rem / 26) - 1) + char : char; + } + + // Map names to HTML ids. For example, idMap['table-col']('Chrome') will return 'table-col-chrome'. + // This is to satisfy the need for unique ids in `headers` attributes. + // Hardcoded array makes it easier to find typos, since it would throw an error if the namespace is mistyped. + const idMap = ['table-group', 'table-col', 'table-row'].reduce((map, namespace) => { + map[namespace] = (str) => namespace + '-' + str.toLowerCase().replace(/[^\w\d-_]+/g, '-'); + return map; + }, {}); + + // Get a copy of the requested SVG icon. Those are defined in the markdown as templates. + function icon(key) { + return document.getElementById(`support-symbol-${key}`).content.firstElementChild.cloneNode(true); + } + + const scrollbox = document.getElementById('feature-support-scrollbox'); + const table = document.getElementById('feature-support'); + + const detectWasmFeature = _loadFeatureDetectModule(); + const addTooltip = _loadTooltipModule(); + + const { features, browsers } = await fetch('/features.json', { + credentials: 'include', // https://stackoverflow.com/a/63814972 + mode: 'no-cors' + }).then(res => res.json()); + + const tBody = document.createElement('tbody'); + table.append( + h('thead', {}, [ + h('tr', {}, [ + h('th', { id: 'table-blank' }), + h('th', { scope: 'col', id: idMap['table-col']('Your browser') }, ['Your browser']), + ...Object.entries(browsers).map(([name, { url, logo }]) => + h('th', { scope: 'col', id: idMap['table-col'](name) }, [ + h('a', { href: url, target: '_blank' }, [ + // https://www.w3.org/WAI/WCAG22/Techniques/html/H2 + h('img', { src: logo, width: 32, height: 32, alt: '' }), + h('br'), + name + ]) + ]) + ) + ]) + ]), + tBody + ); + + let featureGroups = partitionArray( + Object.entries(features).map(([name, feature]) => Object.assign(feature, { name })), + feature => feature.phase >= 4 + ); + + featureGroups = [ + { name: 'Standardized features', features: featureGroups.matched }, + { name: 'In-progress proposals', features: featureGroups.unmatched }, + ]; + + // Collect all notes and assign an index to each unique item + // { "First unique note": 0, "Second unique note": 1, ...} + const notes = Object.values(browsers).flatMap(b => + Object.values(b.features) + .filter(s => Array.isArray(s)) + .map(s => s[1]) + ); + const note2index = new Map(); + let noteIndex = 0; + for (const note of notes) { + if (!note2index.has(note)) { + note2index.set(note, noteIndex++); + } + } + + // Generate the footnote list. They are later referenced in the actual table. + const noteList = document.createElement('ol'); + // Place footnote list outside of the scolling area + scrollbox.parentNode.insertBefore(noteList, scrollbox.nextSibling); + for (const [note, index] of note2index) { + const item = h('li', { id: `feature-note-${index}` }); + noteList.appendChild(item).appendChild(renderNote(note)); + } + + // Create an element that links to the specified footnote. + // Also returns the HTML id of the footnote it refers to. + function createNoteRef(index) { + const id = `feature-note-${index}`; + return [id, h('a', { href: `#${id}` }, [`[${toAlphabet(index)}]`])]; + } + + const columnCount = 2 + Object.keys(browsers).length; + + for (const { name: groupName, features } of featureGroups) { + tBody.append( + h('tr', {}, [ + h('th', { + scope: 'colgroup', + colSpan: columnCount, + id: idMap['table-group'](groupName), + headers: 'table-blank', + // Chrome doesn't handle `headers` attribute correctly. + // Just hide the group headers for now... + // https://bugs.chromium.org/p/chromium/issues/detail?id=1081201 + // + // Actually Firefox doesn't support `ariaHidden` attribute. + // This is a happy coincidence, since `headers` works fine on Firefox anyway. + ariaHidden: true + }, [groupName]) + ]) + ); + for (const { name: featName, description, url } of features) { + const detectResult = h('td', { + headers: [idMap['table-col']('Your browser'), idMap['table-row'](featName)].join(' ') + }, [buildCellInner('loading')]); + + detectWasmFeature(featName).then(supported => { + detectResult.textContent = ''; + detectResult.appendChild(buildCellInner(supported ? 'yes' : 'no')); + addTooltip(detectResult, supported ? '✓ Supported' : '✗ Not supported', [tBody, scrollbox]); + }, _err => { + detectResult.textContent = ''; + detectResult.appendChild(buildCellInner('unknown')); + addTooltip(detectResult, 'Detection unavailable for this feature', [tBody, scrollbox]); + }); + + tBody.append( + h('tr', {}, [ + h('th', { + scope: 'row', + id: idMap['table-row'](featName), + headers: idMap['table-group'](groupName) + }, [h('a', { href: url, target: '_blank' }, [description])]), + detectResult, + ...Object.entries(browsers).map(([browserName, { features }]) => { + // Meaning of each entry: + // * null => not applicable for this browser + // * true/false => supported/unsupported + // * "version" => supported since "version" + // * "flag" => flag required (must be lowercase) + // * [true, "footnotes"] => supported, with "footnotes" + // * ["version", "footnotes"] => supported since "version", with "footnotes" + // ...and any combination thereof + let support = features[featName]; + let box, note; + + // First extract the footnote part if it's an array + if (Array.isArray(support)) { + note = support[1]; + support = support[0]; + } + + if (typeof support === 'string') { + if (support === 'flag') { + // 'flag' is treated specially as the "requires flag" icon + box = buildCellInner('flag'); + } else { + // Otherwise it's a version number, like "95" + box = buildCellInner('yes', support); + note ||= `✓ Supported since version ${support}`; + } + } else if (!support) { + if (support === null) { + box = buildCellInner('na', 'N/A'); + note ||= '✗ Not applicable for this browser/engine'; + } else { + box = buildCellInner('no'); + note ||= '✗ Not supported'; + } + } else { + if (support !== true) throw new TypeError(); + box = buildCellInner('yes'); + // Magic value, keep in sync with `renderNote` + note ||= '✓ Supported, introduced in unknown version'; + } + + const cell = h('td', { + headers: [idMap['table-col'](browserName), idMap['table-row'](featName)].join(' ') + }, [box]); + + // Give the cell itself an `aria-lebel` to avoid screen readers calling it "empty cell". + const icon = box.firstElementChild; + if (icon?.hasAttribute('aria-label')) { + cell.setAttribute('aria-label', icon.getAttribute('aria-label')); + icon.removeAttribute('aria-label'); + } + + if (note && note2index.has(note)) { + cell.tabIndex = 0; // focusable + const index = note2index.get(note); + const [noteId, refLink] = createNoteRef(index); + box.appendChild(h('sup', {}, [refLink])); + + const noteItem = document.getElementById(noteId); + if (noteItem) { + cell.addEventListener('mouseenter', () => noteItem.classList.add('ref-highlight')); + cell.addEventListener('mouseleave', () => noteItem.classList.remove('ref-highlight')); + } + } + + // Clip to both and the scrollbox. + // the former is to avoid blocking out the headers; + // the latter is to keep the tooltip inside the scrollable area + addTooltip(cell, note, [tBody, scrollbox]); + return cell; + }) + ]) + ); + tBody.lastElementChild.setAttribute('aria-describedby', idMap['table-row'](featName)); + } + } + + function buildCellInner(type, text) { + const content = text || icon(type); + return h('div', { className: `feature-cell icon-${type}`}, [content]); + } + + function renderNote(note) { + const fragment = document.createDocumentFragment(); + const isMissingData = note.includes('introduced in unknown version'); + + // Transform markdown-like backticks into html + while (note) { + const [head, body, tail] = splitParts(note, '`'); + head && fragment.append(head); + body && fragment.appendChild(h('code', {}, [body])); + note = tail; + } + + const firstNode = fragment.firstChild; + if (firstNode.nodeType === Node.TEXT_NODE) { + // No point for screen readers to pronounce those symbols out loud. + for (const symbol of ['✓', '✗']) { + if (firstNode.nodeValue?.startsWith(symbol)) { + // Before: <#text>✓ Supported + // After: <#text> Supported + firstNode.splitText(1); + const symbolNode = h('span', {}, firstNode.nodeValue); + symbolNode.setAttribute('aria-hidden', ''); + fragment.replaceChild(symbolNode, firstNode); + break; + } + } + } + + if (isMissingData) { + fragment.appendChild(h('a', { + href: 'https://github.com/WebAssembly/website/blob/master/features.json', + target: '_blank' + }, [' (contribute data)'])) + } + + return fragment; + } + + // Break a string into three parts using the given delimiter. + function splitParts(str, delim) { + const start = str.indexOf(delim); + const end = str.indexOf(delim, start + 1); + if (start >= 0 && end > start) { + const head = str.substring(0, start); + const body = str.substring(start + 1, end); + const tail = str.substring(end + 1); + return [head, body, tail]; + } + return [str, '', ''] + } + + // Lazy-loading + function _loadTooltipModule() { + // Be sure to change the preloads in markdown when updating url. + // The ESM bundle of this package doesn't work with unpkg.com. + const module = import('https://cdn.jsdelivr.net/npm/@floating-ui/dom@1/+esm'); + + const subscribers = new Set(); + const updateAll = () => { for (const fn of subscribers) fn(); }; + + document.addEventListener('scroll', updateAll, { passive: true }); + scrollbox.addEventListener('scroll', updateAll, { passive: true }); + window.addEventListener('resize', updateAll, { passive: true }); + + let counter = 0; + return (reference, note, boundary) => + module.then(({ computePosition, offset, flip, shift, arrow }) => { + const tooltipId = `tooltip-${counter++}`; + const tooltip = h('div', { id: tooltipId, className: 'feature-tooltip', role: 'tooltip' }); + tooltip.appendChild(renderNote(note)); + + const arrowElement = h('div', { className: 'feature-tooltip-arrow' }); + tooltip.appendChild(arrowElement); + + const update = () => computePosition(reference, tooltip, { + placement: 'top', + middleware: [ + offset(6), + flip({ boundary }), + shift({ padding: 6, boundary }), + arrow({ element: arrowElement, padding: 3, boundary }) + ], + }).then(({ x, y, placement, middlewareData }) => { + const { x: arrowX, y: arrowY } = middlewareData.arrow; + Object.assign(arrowElement.style, { + left: arrowX !== null ? `${arrowX}px` : '', + top: arrowY !== null ? `${arrowY}px` : '', + }); + + tooltip.style.transform = `translate(${x}px, ${y}px)`; + // Force the browser to apply CSS changes first + if (tooltip.dataset.placement !== placement) tooltip.offsetHeight; + // This will then enable the transition effect + tooltip.dataset.placement = placement; + }); + + const setVisible = (visible) => { + if (visible) { + tooltip.style.removeProperty('display'); + update(); + subscribers.add(update); + } else { + subscribers.delete(update); + tooltip.style.display = 'none'; + delete tooltip.dataset.placement; // disable the transition effect + } + }; + + setVisible(false); + + const monitor = (name, state, listener = () => setVisible(state)) => + reference.addEventListener(name, listener); + monitor('focusin', true); + monitor('focusout', false); + + // Add a bit of delay to mouse events + let timeout = null; + monitor('mouseenter', true, () => { + clearTimeout(timeout); + if (subscribers.size) { + timeout = setTimeout(() => setVisible(true), 80); + } else { + // Immediately show if there aren't other tooltips visible + setVisible(true); + } + }); + monitor('mouseleave', false, () => { + clearTimeout(timeout); + timeout = setTimeout(() => setVisible(false), 80); + }); + + reference.appendChild(tooltip); + reference.setAttribute('aria-describedby', tooltipId); + return tooltip; + }); + } + + function _loadFeatureDetectModule() { + // Be sure to change the preloads in markdown when updating url. + const module = import('https://cdn.jsdelivr.net/npm/wasm-feature-detect@1/dist/esm/index.js'); + return (featureName) => module + .then(wasmFeatureDetect => wasmFeatureDetect[featureName]()); + } +})(); diff --git a/roadmap.md b/roadmap.md index 0780fabf..ee201218 100644 --- a/roadmap.md +++ b/roadmap.md @@ -7,106 +7,32 @@ In November 2017, WebAssembly CG members representing four browsers, Chrome, Edg After the initial release, WebAssembly has been gaining new features through the [standardization process](https://github.com/WebAssembly/meetings/blob/master/process/phases.md). For the complete list of current proposals and their respective stages, check out the [`WebAssembly/proposals` repo](https://github.com/WebAssembly/proposals). - - -
The table below aims to track implemented features in popular engines:
- - + + + + + + + + +The table below aims to track implemented features in popular engines: + +
+
+
+ + + + To detect supported features at runtime from JavaScript, check out the [`wasm-feature-detect` library](https://github.com/GoogleChromeLabs/wasm-feature-detect), which powers the "Your browser" column above.