a post-component library for building user-interfaces on the web.

improve accuracy of "incorrect interpolation" warning (#35)

and skip over interpolations in comments, just to acknowledge they're
there, so you can comment out code and not face the warning.

authored by tombl.dev and committed by

GitHub f4eead8f 7a47c3b5

+31 -24
+24 -6
src/html.js
··· 13 13 14 14 /** @type {typeof NodeFilter.SHOW_ELEMENT} */ const NODE_FILTER_ELEMENT = 1 15 15 /** @type {typeof NodeFilter.SHOW_TEXT} */ const NODE_FILTER_TEXT = 4 16 + /** @type {typeof NodeFilter.SHOW_COMMENT} */ const NODE_FILTER_COMMENT = 128 16 17 17 18 /** @return {node is Element} */ 18 19 const isElement = node => node.nodeType === /** @satisfies {typeof Node.ELEMENT_NODE} */ (1) 19 20 20 21 /** @return {node is Text} */ 21 22 const isText = node => node.nodeType === /** @satisfies {typeof Node.TEXT_NODE} */ (3) 23 + 24 + /** @return {node is Comment} */ 25 + const isComment = node => node.nodeType === /** @satisfies {typeof Node.COMMENT_NODE} */ (8) 22 26 23 27 /** @return {node is DocumentFragment} */ 24 28 const isDocumentFragment = node => node.nodeType === /** @satisfies {typeof Node.DOCUMENT_FRAGMENT_NODE} */ (11) ··· 124 128 this.#statics = statics 125 129 this._dynamics = dynamics 126 130 127 - // just to check for errors 128 - if (DEV) compileTemplate(statics) 131 + // eagerly compile the template in DEV, plus some extra checks. 132 + DEV: assert( 133 + this._template._parts.length === dynamics.length, 134 + 'expected the same number of dynamics as parts. do you have a ${...} in an unsupported place?', 135 + ) 129 136 } 130 137 } 131 138 ··· 238 245 if (isDocumentFragment(node)) compiled._rootParts.push(nextPart) 239 246 else if ('dynparts' in node.dataset) node.dataset.dynparts += ' ' + nextPart 240 247 else node.dataset.dynparts = nextPart 241 - if (DEV && nextPart !== idx) console.warn('dynamic value detected in static location') 242 248 compiled._parts[nextPart++] = [idx, createPart] 243 249 } 244 250 245 - const walker = document.createTreeWalker(templateElement.content, NODE_FILTER_TEXT | NODE_FILTER_ELEMENT) 251 + const walker = document.createTreeWalker( 252 + templateElement.content, 253 + NODE_FILTER_TEXT | NODE_FILTER_ELEMENT | (DEV ? NODE_FILTER_COMMENT : 0), 254 + ) 246 255 // stop iterating once we've hit the last part, but if we're in dev mode, keep going to check for mistakes. 247 - while ((DEV || nextPart < compiled._parts.length) && walker.nextNode()) { 248 - const node = /** @type {Text | Element} */ (walker.currentNode) 256 + while ((nextPart < compiled._parts.length || DEV) && walker.nextNode()) { 257 + const node = /** @type {Text | Element | Comment} */ (walker.currentNode) 249 258 if (isText(node)) { 250 259 const nodes = [...node.data.matchAll(DYNAMIC_GLOBAL)].reverse().map(match => { 251 260 node.splitText(match.index + match[0].length) ··· 266 275 patch(node.parentNode, idx, (_prev, span) => new ChildPart(child, span)) 267 276 } 268 277 } 278 + } else if (DEV && isComment(node)) { 279 + // just in dev, stub out a fake part for every interpolation in a comment. 280 + // this means you can comment out code inside a template and not run into 281 + // issues with incorrect part counts. 282 + // in production the check is skipped, so we can also skip this. 283 + for (const _match of node.data.matchAll(DYNAMIC_GLOBAL)) { 284 + compiled._parts[nextPart++] = [parseInt(_match[1]), () => ({ create() {}, update() {}, detach() {} })] 285 + } 269 286 } else { 287 + assert(isElement(node)) 270 288 const toRemove = [] 271 289 for (let name of node.getAttributeNames()) { 272 290 const value = node.getAttribute(name)
+1 -1
test/attributes.test.ts
··· 35 35 expect(el.querySelector('h1')).toHaveClass('foo') 36 36 }) 37 37 38 - it('throws on property attributes without dynamic values', () => { 38 + it('throws on property attributes without dynamic values', { skip: import.meta.env.PROD }, () => { 39 39 const { root } = setup() 40 40 41 41 expect(() => {
+6 -17
test/basic.test.ts
··· 112 112 }) 113 113 }) 114 114 115 - const console = { 116 - warn: vi.fn(), 117 - } 118 - vi.stubGlobal('console', console) 119 - 120 115 describe('errors', () => { 121 116 it('throws on non-function event handlers', { skip: import.meta.env.PROD }, () => { 122 117 const { root, el } = setup() ··· 155 150 expect(el.innerHTML).toBe('<!---->') 156 151 }) 157 152 158 - it('warns on invalid part placement', () => { 153 + it('warns on invalid part placement', { skip: import.meta.env.PROD }, () => { 159 154 const { root, el } = setup() 160 155 161 - expect(console.warn).not.toHaveBeenCalled() 162 - 163 - root.render(html`<${'div'}>${'text'}</${'div'}>`) 164 - expect(el.innerHTML).toMatchInlineSnapshot(`"<dyn-$0>text</dyn-$0>"`) 165 - 166 - expect(console.warn).toHaveBeenCalledWith('dynamic value detected in static location') 156 + expect(() => root.render(html`<${'div'}>${'text'}</${'div'}>`)).toThrowErrorMatchingInlineSnapshot( 157 + `[Error: expected the same number of dynamics as parts. do you have a \${...} in an unsupported place?]`, 158 + ) 159 + expect(el.innerHTML).toBe('') 167 160 }) 168 161 169 - it('does not warn parts in comments', () => { 162 + it('does not throw on parts in comments', () => { 170 163 const { root, el } = setup() 171 164 172 - expect(console.warn).not.toHaveBeenCalled() 173 - 174 165 root.render(html`<!-- ${'text'} -->`) 175 166 expect(el.innerHTML).toMatchInlineSnapshot(`"<!-- dyn-$0 -->"`) 176 - 177 - expect(console.warn).not.toHaveBeenCalled() 178 167 }) 179 168 })