From 14fe89eae89f30d0993f45ea978519f3624f2344 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 17 Jun 2017 14:31:19 -0400 Subject: [PATCH] hydration working with elements, text nodes, tags and if blocks --- src/generators/dom/Block.ts | 65 ++++++++----- src/generators/dom/index.ts | 4 +- src/generators/dom/interfaces.ts | 1 + src/generators/dom/preprocess.ts | 6 +- .../dom/visitors/Element/Element.ts | 91 +++++++++++++------ src/generators/dom/visitors/Element/Ref.ts | 4 +- src/generators/dom/visitors/IfBlock.ts | 61 ++++++------- src/generators/dom/visitors/MustacheTag.ts | 1 + src/generators/dom/visitors/Text.ts | 33 ++----- src/shared/dom.js | 36 ++++---- .../samples/element-nested/_after.html | 3 + .../samples/element-nested/_before.html | 3 + .../samples/element-nested/_config.js | 17 ++++ .../samples/element-nested/main.html | 3 + test/hydration/samples/if-block/_after.html | 1 + test/hydration/samples/if-block/_before.html | 1 + test/hydration/samples/if-block/_config.js | 19 ++++ test/hydration/samples/if-block/main.html | 3 + .../samples/top-level-text/_after.html | 1 + .../samples/top-level-text/_before.html | 1 + .../samples/top-level-text/_config.js | 13 +++ .../samples/top-level-text/main.html | 1 + 22 files changed, 231 insertions(+), 137 deletions(-) create mode 100644 test/hydration/samples/element-nested/_after.html create mode 100644 test/hydration/samples/element-nested/_before.html create mode 100644 test/hydration/samples/element-nested/_config.js create mode 100644 test/hydration/samples/element-nested/main.html create mode 100644 test/hydration/samples/if-block/_after.html create mode 100644 test/hydration/samples/if-block/_before.html create mode 100644 test/hydration/samples/if-block/_config.js create mode 100644 test/hydration/samples/if-block/main.html create mode 100644 test/hydration/samples/top-level-text/_after.html create mode 100644 test/hydration/samples/top-level-text/_before.html create mode 100644 test/hydration/samples/top-level-text/_config.js create mode 100644 test/hydration/samples/top-level-text/main.html diff --git a/src/generators/dom/Block.ts b/src/generators/dom/Block.ts index 7b7a19eb10..06a6320cc2 100644 --- a/src/generators/dom/Block.ts +++ b/src/generators/dom/Block.ts @@ -40,7 +40,9 @@ export default class Block { listName: string; builders: { + init: CodeBuilder; create: CodeBuilder; + hydrate: CodeBuilder; mount: CodeBuilder; intro: CodeBuilder; update: CodeBuilder; @@ -86,7 +88,9 @@ export default class Block { this.listName = options.listName; this.builders = { + init: new CodeBuilder(), create: new CodeBuilder(), + hydrate: new CodeBuilder(), mount: new CodeBuilder(), intro: new CodeBuilder(), update: new CodeBuilder(), @@ -111,7 +115,7 @@ export default class Block { this.hasUpdateMethod = false; // determined later } - addDependencies(dependencies) { + addDependencies(dependencies: string[]) { dependencies.forEach(dependency => { this.dependencies.add(dependency); }); @@ -120,21 +124,17 @@ export default class Block { addElement( name: string, renderStatement: string, + hydrateStatement: string, parentNode: string, needsIdentifier = false ) { const isToplevel = !parentNode; - if (needsIdentifier || isToplevel) { - this.builders.create.addLine(`var ${name} = ${renderStatement};`); - this.mount(name, parentNode); - } else { - this.builders.create.addLine( - `${this.generator.helper( - 'appendNode' - )}( ${renderStatement}, ${parentNode} );` - ); - } + this.addVariable(name); + this.builders.create.addLine(`${name} = ${renderStatement};`); + this.builders.hydrate.addLine(`${name} = ${hydrateStatement};`) + + this.mount(name, parentNode); if (isToplevel) { this.builders.unmount.addLine( @@ -184,7 +184,7 @@ export default class Block { mount(name: string, parentNode: string) { if (parentNode) { - this.builders.create.addLine( + this.builders.mount.addLine( `${this.generator.helper('appendNode')}( ${name}, ${parentNode} );` ); } else { @@ -210,19 +210,8 @@ export default class Block { this.addVariable(outroing); } - if (this.variables.size) { - const variables = Array.from(this.variables.keys()) - .map(key => { - const init = this.variables.get(key); - return init !== undefined ? `${key} = ${init}` : key; - }) - .join(', '); - - this.builders.create.addBlockAtStart(`var ${variables};`); - } - if (this.autofocus) { - this.builders.create.addLine(`${this.autofocus}.focus();`); + this.builders.mount.addLine(`${this.autofocus}.focus();`); } // minor hack – we need to ensure that any {{{triples}}} are detached first @@ -240,6 +229,26 @@ export default class Block { properties.addBlock(`first: ${this.first},`); } + if (this.builders.create.isEmpty()) { + properties.addBlock(`create: ${this.generator.helper('noop')},`); + } else { + properties.addBlock(deindent` + create: function () { + ${this.builders.create} + }, + `); + } + + if (this.builders.hydrate.isEmpty()) { + properties.addBlock(`hydrate: ${this.generator.helper('noop')},`); + } else { + properties.addBlock(deindent` + hydrate: function ( nodes ) { + ${this.builders.hydrate} + }, + `); + } + if (this.builders.mount.isEmpty()) { properties.addBlock(`mount: ${this.generator.helper('noop')},`); } else { @@ -331,7 +340,13 @@ export default class Block { .key ? `, ${localKey}` : ''} ) { - ${this.builders.create} + ${this.variables.size > 0 && ( + `var ${Array.from(this.variables.keys()).map(key => { + const init = this.variables.get(key); + return init !== undefined ? `${key} = ${init}` : key; + }).join(', ')};`)} + + ${!this.builders.init.isEmpty() && this.builders.init} return { ${properties} diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index 2252126861..cc54419cbe 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -233,7 +233,9 @@ export default function dom( this._fragment = ${generator.alias( 'create_main_fragment' - )}( options.target, 0, this._state, this ); + )}( this._state, this ); + this._fragment.hydrate( ${generator.helper('children')}( options.target ) ); + this._fragment.mount( options.target, null ); ${generator.hasComplexBindings && `while ( this._bindings.length ) this._bindings.pop()();`} ${(generator.hasComponents || generator.hasIntroTransitions) && diff --git a/src/generators/dom/interfaces.ts b/src/generators/dom/interfaces.ts index 625f919c28..03923b79bf 100644 --- a/src/generators/dom/interfaces.ts +++ b/src/generators/dom/interfaces.ts @@ -2,6 +2,7 @@ export interface State { name: string; namespace: string; parentNode: string; + parentNodes: string; isTopLevel: boolean; parentNodeName?: string; basename?: string; diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts index 1fabe3a12d..dc3ff59717 100644 --- a/src/generators/dom/preprocess.ts +++ b/src/generators/dom/preprocess.ts @@ -12,7 +12,7 @@ function isElseIf(node: Node) { } function getChildState(parent: State, child = {}) { - return assign({}, parent, { name: null, parentNode: null }, child || {}); + return assign({}, parent, { name: null, parentNode: null, parentNodes: 'nodes' }, child || {}); } // Whitespace inside one of these elements will not result in @@ -285,6 +285,7 @@ const preprocessors = { isTopLevel: false, name, parentNode: name, + parentNodes: block.getUniqueName(`${name}_nodes`), parentNodeName: node.name, namespace: node.name === 'svg' ? 'http://www.w3.org/2000/svg' @@ -388,7 +389,7 @@ export default function preprocess( indexes: new Map(), contextDependencies: new Map(), - params: ['target', 'h', 'state'], + params: ['state'], indexNames: new Map(), listNames: new Map(), @@ -398,6 +399,7 @@ export default function preprocess( const state: State = { namespace, parentNode: null, + parentNodes: 'nodes', isTopLevel: true, }; diff --git a/src/generators/dom/visitors/Element/Element.ts b/src/generators/dom/visitors/Element/Element.ts index 2f701dff6b..7d7694733a 100644 --- a/src/generators/dom/visitors/Element/Element.ts +++ b/src/generators/dom/visitors/Element/Element.ts @@ -47,14 +47,44 @@ export default function visitElement( const childState = node._state; const name = childState.parentNode; - block.builders.create.addLine( - `var ${name} = ${getRenderStatement( - generator, - childState.namespace, - node.name - )};` - ); - block.mount(name, state.parentNode); + const isToplevel = !state.parentNode; + + block.addVariable(name); + block.builders.create.addLine(`${name} = ${getRenderStatement(generator, childState.namespace, node.name)};`); + block.builders.hydrate.addBlock(deindent` + ${name} = ${getHydrateStatement(generator, childState.namespace, state.parentNodes, node.name)}; + var ${childState.parentNodes} = ${generator.helper('children')}( ${name} ) + `); + + if (state.parentNode) { + block.builders.mount.addLine(`${block.generator.helper('appendNode')}( ${name}, ${state.parentNode} );`); + } else { + block.builders.mount.addLine(`${block.generator.helper('insertNode')}( ${name}, ${block.target}, anchor );`); + } + + if (isToplevel) { + block.builders.unmount.addLine( + `${block.generator.helper('detachNode')}( ${name} );` + ); + } + + // block.addVariable(name); + + // block.builders.create.addLine( + // `${name} = ${getRenderStatement( + // generator, + // childState.namespace, + // node.name + // )};` + // ); + + // block.builders.hydrate.addLine( + // `${name} = ${getHydrateStatement( + // generator, + // childState.namespace, + // node.name + // )};` + // ); // add CSS encapsulation attribute if (generator.cssId && (!generator.cascade || state.isTopLevel)) { @@ -178,34 +208,35 @@ export default function visitElement( } } -// function getRenderStatement( -// generator: DomGenerator, -// namespace: string, -// name: string -// ) { -// if (namespace === 'http://www.w3.org/2000/svg') { -// return `${generator.helper('createSvgElement')}( '${name}' )`; -// } +function getRenderStatement( + generator: DomGenerator, + namespace: string, + name: string +) { + if (namespace === 'http://www.w3.org/2000/svg') { + return `${generator.helper('createSvgElement')}( '${name}' )`; + } -// if (namespace) { -// return `document.createElementNS( '${namespace}', '${name}' )`; -// } + if (namespace) { + return `document.createElementNS( '${namespace}', '${name}' )`; + } -// return `${generator.helper('createElement')}( '${name}' )`; -// } + return `${generator.helper('createElement')}( '${name}' )`; +} -function getRenderStatement( +function getHydrateStatement( generator: DomGenerator, namespace: string, + nodes: string, name: string ) { - // if (namespace === 'http://www.w3.org/2000/svg') { - // return `${generator.helper('createSvgElement')}( '${name}' )`; - // } + if (namespace === 'http://www.w3.org/2000/svg') { + return `${generator.helper('claimSvgElement')}( '${name}' )`; + } - // if (namespace) { - // return `document.createElementNS( '${namespace}', '${name}' )`; - // } + if (namespace) { + throw new Error('TODO hydrate exotic namespaces'); + } - return `${generator.helper('hydrateElement')}( target, h, '${name.toUpperCase()}' )`; -} + return `${generator.helper('claimElement')}( ${nodes}, '${name.toUpperCase()}' )`; +} \ No newline at end of file diff --git a/src/generators/dom/visitors/Element/Ref.ts b/src/generators/dom/visitors/Element/Ref.ts index 524c168e2b..53b808c97e 100644 --- a/src/generators/dom/visitors/Element/Ref.ts +++ b/src/generators/dom/visitors/Element/Ref.ts @@ -13,11 +13,11 @@ export default function visitRef( ) { const name = attribute.name; - block.builders.create.addLine( + block.builders.mount.addLine( `${block.component}.refs.${name} = ${state.parentNode};` ); - block.builders.destroy.addLine(deindent` + block.builders.unmount.addLine(deindent` if ( ${block.component}.refs.${name} === ${state.parentNode} ) ${block.component}.refs.${name} = null; `); diff --git a/src/generators/dom/visitors/IfBlock.ts b/src/generators/dom/visitors/IfBlock.ts index e36c5d9984..5a40541031 100644 --- a/src/generators/dom/visitors/IfBlock.ts +++ b/src/generators/dom/visitors/IfBlock.ts @@ -105,10 +105,19 @@ export default function visitIfBlock( simple(generator, block, state, node, branches[0], dynamic, vars); } + block.builders.create.addLine( + `${name}.create();` + ); + + block.builders.hydrate.addLine( + `${name}.hydrate( ${state.parentNodes} );` + ); + if (node.needsAnchor) { block.addElement( anchor, `${generator.helper('createComment')}()`, + `${generator.helper('createComment')}()`, state.parentNode, true ); @@ -126,22 +135,18 @@ function simple( dynamic, { name, anchor, params, if_name } ) { - block.builders.create.addBlock(deindent` + block.builders.init.addBlock(deindent` var ${name} = (${branch.condition}) && ${branch.block}( ${params}, ${block.component} ); `); const isTopLevel = !state.parentNode; const mountOrIntro = branch.hasIntroMethod ? 'intro' : 'mount'; + const targetNode = state.parentNode || block.target; + const anchorNode = state.parentNode ? 'null' : 'anchor'; - if (isTopLevel) { - block.builders.mount.addLine( - `if ( ${name} ) ${name}.${mountOrIntro}( ${block.target}, anchor );` - ); - } else { - block.builders.create.addLine( - `if ( ${name} ) ${name}.${mountOrIntro}( ${state.parentNode}, null );` - ); - } + block.builders.mount.addLine( + `if ( ${name} ) ${name}.${mountOrIntro}( ${block.target}, anchor );` + ); const parentNode = state.parentNode || `${anchor}.parentNode`; @@ -218,7 +223,7 @@ function compound( const current_block = block.getUniqueName(`current_block`); const current_block_and = hasElse ? '' : `${current_block} && `; - block.builders.create.addBlock(deindent` + block.builders.init.addBlock(deindent` function ${get_block} ( ${params} ) { ${branches .map(({ condition, block }) => { @@ -234,15 +239,11 @@ function compound( const isTopLevel = !state.parentNode; const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount'; - if (isTopLevel) { - block.builders.mount.addLine( - `${if_name}${name}.${mountOrIntro}( ${block.target}, anchor );` - ); - } else { - block.builders.create.addLine( - `${if_name}${name}.${mountOrIntro}( ${state.parentNode}, null );` - ); - } + const targetNode = state.parentNode || block.target; + const anchorNode = state.parentNode ? 'null' : 'anchor'; + block.builders.mount.addLine( + `${if_name}${name}.${mountOrIntro}( ${targetNode}, ${anchorNode} );` + ); const parentNode = state.parentNode || `${anchor}.parentNode`; @@ -304,7 +305,7 @@ function compoundWithOutros( block.addVariable(current_block_index); block.addVariable(name); - block.builders.create.addBlock(deindent` + block.builders.init.addBlock(deindent` var ${if_block_creators} = [ ${branches.map(branch => branch.block).join(',\n')} ]; @@ -323,12 +324,12 @@ function compoundWithOutros( `); if (hasElse) { - block.builders.create.addBlock(deindent` + block.builders.init.addBlock(deindent` ${current_block_index} = ${get_block}( ${params} ); ${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} ); `); } else { - block.builders.create.addBlock(deindent` + block.builders.init.addBlock(deindent` if ( ~( ${current_block_index} = ${get_block}( ${params} ) ) ) { ${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} ); } @@ -337,16 +338,12 @@ function compoundWithOutros( const isTopLevel = !state.parentNode; const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount'; + const targetNode = state.parentNode || block.target; + const anchorNode = state.parentNode ? 'null' : 'anchor'; - if (isTopLevel) { - block.builders.mount.addLine( - `${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${block.target}, anchor );` - ); - } else { - block.builders.create.addLine( - `${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${state.parentNode}, null );` - ); - } + block.builders.mount.addLine( + `${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${targetNode}, ${anchorNode} );` + ); const parentNode = state.parentNode || `${anchor}.parentNode`; diff --git a/src/generators/dom/visitors/MustacheTag.ts b/src/generators/dom/visitors/MustacheTag.ts index febdb452e9..5fb3d7fc26 100644 --- a/src/generators/dom/visitors/MustacheTag.ts +++ b/src/generators/dom/visitors/MustacheTag.ts @@ -19,6 +19,7 @@ export default function visitMustacheTag( block.addElement( name, `${generator.helper('createText')}( ${value} = ${snippet} )`, + `${generator.helper('claimText')}( ${state.parentNode}_nodes, ${value} = ${snippet} )`, state.parentNode, true ); diff --git a/src/generators/dom/visitors/Text.ts b/src/generators/dom/visitors/Text.ts index 9d316a8ae0..ae0337990c 100644 --- a/src/generators/dom/visitors/Text.ts +++ b/src/generators/dom/visitors/Text.ts @@ -10,32 +10,11 @@ export default function visitText( node: Node ) { if (!node._state.shouldCreate) return; - - const isTopLevel = !state.parentNode; - let h; - if (!isTopLevel) { - h = block.getUniqueName(`${state.parentNode}_i`) - block.addVariable(h, 0); - } else { - h = block.alias('h'); - } - - const prefix = state.parentNode && !node.usedAsAnchor ? '' : `var ${node._state.name} = `; - - block.builders.create.addLine( - `${prefix}${generator.helper('hydrateText')}( ${state.parentNode || 'target'}, ${h}++, ${JSON.stringify(node.data)} )` + block.addElement( + node._state.name, + `${generator.helper('createText')}( ${JSON.stringify(node.data)} )`, + `${generator.helper('claimText')}( ${state.parentNodes}, ${JSON.stringify(node.data)} )`, + state.parentNode, + node.usedAsAnchor ); - - if (!state.parentNode) { - this.builders.unmount.addLine( - `${this.generator.helper('detachNode')}( ${name} );` - ); - } - - // block.addElement( - // node._state.name, - // `${generator.helper('hydrateText')}( ${state.parentNode}, 0, ${JSON.stringify(node.data)} )`, - // state.parentNode, - // node.usedAsAnchor - // ); } diff --git a/src/shared/dom.js b/src/shared/dom.js index 685923f73d..11d5395b89 100644 --- a/src/shared/dom.js +++ b/src/shared/dom.js @@ -67,30 +67,30 @@ export function toNumber(value) { return value === '' ? undefined : +value; } -export function hydrateElement(target, i, type) { // TODO attrs - var child; - while (child = target.childNodes[i]) { - if (child.nodeName === type) { - return child; +export function children ( element ) { + return Array.from(element.childNodes); +} + +export function claimElement ( nodes, name ) { + for (var i = 0; i < nodes.length; i += 1) { + var node = nodes[i]; + if (node.nodeName === name) { + return nodes.splice(i, 1)[0]; // TODO strip unwanted attributes } - target.removeChild(child); } - child = createElement(type); - target.appendChild(child); - return child; + console.trace('creating', name); + return createElement(name); } -export function hydrateText(target, i, data) { - var child; - while (child = target.childNodes[i]) { - if (child.nodeType === 3) { - return (child.data = data, child); +export function claimText ( nodes, data ) { + for (var i = 0; i < nodes.length; i += 1) { + var node = nodes[i]; + if (node.nodeType === 3) { + node.data = data; + return nodes.splice(i, 1)[0]; } - target.removeChild(child); } - child = createText(data); - target.appendChild(child); - return child; + return createText(data); } \ No newline at end of file diff --git a/test/hydration/samples/element-nested/_after.html b/test/hydration/samples/element-nested/_after.html new file mode 100644 index 0000000000..b96db6ffc8 --- /dev/null +++ b/test/hydration/samples/element-nested/_after.html @@ -0,0 +1,3 @@ +
+

nested

+
\ No newline at end of file diff --git a/test/hydration/samples/element-nested/_before.html b/test/hydration/samples/element-nested/_before.html new file mode 100644 index 0000000000..b96db6ffc8 --- /dev/null +++ b/test/hydration/samples/element-nested/_before.html @@ -0,0 +1,3 @@ +
+

nested

+
\ No newline at end of file diff --git a/test/hydration/samples/element-nested/_config.js b/test/hydration/samples/element-nested/_config.js new file mode 100644 index 0000000000..0965bc39c6 --- /dev/null +++ b/test/hydration/samples/element-nested/_config.js @@ -0,0 +1,17 @@ +export default { + snapshot(target) { + const div = target.querySelector('div'); + + return { + div, + p: div.querySelector('p') + }; + }, + + test(assert, target, snapshot) { + const div = target.querySelector('div'); + + assert.equal(div, snapshot.div); + assert.equal(div.querySelector('p'), snapshot.p); + } +}; \ No newline at end of file diff --git a/test/hydration/samples/element-nested/main.html b/test/hydration/samples/element-nested/main.html new file mode 100644 index 0000000000..b96db6ffc8 --- /dev/null +++ b/test/hydration/samples/element-nested/main.html @@ -0,0 +1,3 @@ +
+

nested

+
\ No newline at end of file diff --git a/test/hydration/samples/if-block/_after.html b/test/hydration/samples/if-block/_after.html new file mode 100644 index 0000000000..5ed8b34a53 --- /dev/null +++ b/test/hydration/samples/if-block/_after.html @@ -0,0 +1 @@ +

foo!

\ No newline at end of file diff --git a/test/hydration/samples/if-block/_before.html b/test/hydration/samples/if-block/_before.html new file mode 100644 index 0000000000..5ed8b34a53 --- /dev/null +++ b/test/hydration/samples/if-block/_before.html @@ -0,0 +1 @@ +

foo!

\ No newline at end of file diff --git a/test/hydration/samples/if-block/_config.js b/test/hydration/samples/if-block/_config.js new file mode 100644 index 0000000000..465c32c2af --- /dev/null +++ b/test/hydration/samples/if-block/_config.js @@ -0,0 +1,19 @@ +export default { + data: { + foo: true + }, + + snapshot(target) { + const p = target.querySelector('p'); + + return { + p + }; + }, + + test(assert, target, snapshot) { + const p = target.querySelector('p'); + + assert.equal(p, snapshot.p); + } +}; \ No newline at end of file diff --git a/test/hydration/samples/if-block/main.html b/test/hydration/samples/if-block/main.html new file mode 100644 index 0000000000..050095b913 --- /dev/null +++ b/test/hydration/samples/if-block/main.html @@ -0,0 +1,3 @@ +{{#if foo}} +

foo!

+{{/if}} \ No newline at end of file diff --git a/test/hydration/samples/top-level-text/_after.html b/test/hydration/samples/top-level-text/_after.html new file mode 100644 index 0000000000..4add785daf --- /dev/null +++ b/test/hydration/samples/top-level-text/_after.html @@ -0,0 +1 @@ +Text \ No newline at end of file diff --git a/test/hydration/samples/top-level-text/_before.html b/test/hydration/samples/top-level-text/_before.html new file mode 100644 index 0000000000..4add785daf --- /dev/null +++ b/test/hydration/samples/top-level-text/_before.html @@ -0,0 +1 @@ +Text \ No newline at end of file diff --git a/test/hydration/samples/top-level-text/_config.js b/test/hydration/samples/top-level-text/_config.js new file mode 100644 index 0000000000..e8a81e7ca1 --- /dev/null +++ b/test/hydration/samples/top-level-text/_config.js @@ -0,0 +1,13 @@ +export default { + snapshot(target) { + return { + text: target.childNodes[0] + }; + }, + + test(assert, target, snapshot) { + const text = target.childNodes[0]; + + assert.equal(text, snapshot.text); + } +}; \ No newline at end of file diff --git a/test/hydration/samples/top-level-text/main.html b/test/hydration/samples/top-level-text/main.html new file mode 100644 index 0000000000..4add785daf --- /dev/null +++ b/test/hydration/samples/top-level-text/main.html @@ -0,0 +1 @@ +Text \ No newline at end of file