UNPKG

22.6 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.log = log;
7exports.setStateOptions = exports.createReactComponentImportDeclaration = exports.booleanOption = exports.processCss = exports.combinePlugins = exports.addSourceMaps = exports.makeSourceMapGenerator = exports.makeStyledJsxTag = exports.cssToBabelType = exports.templateLiteralFromPreprocessedCss = exports.computeClassNames = exports.getJSXStyleInfo = exports.validateExternalExpressions = exports.isDynamic = exports.validateExpressionVisitor = exports.findStyles = exports.isStyledJsx = exports.isGlobalEl = exports.getScope = exports.addClassName = exports.hashString = void 0;
8
9var _path = _interopRequireDefault(require("path"));
10
11var t = _interopRequireWildcard(require("babel-types"));
12
13var _stringHash = _interopRequireDefault(require("string-hash"));
14
15var _sourceMap = require("source-map");
16
17var _convertSourceMap = _interopRequireDefault(require("convert-source-map"));
18
19var _styleTransform = _interopRequireDefault(require("./lib/style-transform"));
20
21var _constants = require("./_constants");
22
23function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }
24
25function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
26
27function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }
28
29function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
30
31function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
32
33var concat = function concat(a, b) {
34 return t.binaryExpression('+', a, b);
35};
36
37var and = function and(a, b) {
38 return t.logicalExpression('&&', a, b);
39};
40
41var or = function or(a, b) {
42 return t.logicalExpression('||', a, b);
43};
44
45var joinSpreads = function joinSpreads(spreads) {
46 return spreads.reduce(function (acc, curr) {
47 return or(acc, curr);
48 });
49};
50
51var hashString = function hashString(str) {
52 return String((0, _stringHash.default)(str));
53};
54
55exports.hashString = hashString;
56
57var addClassName = function addClassName(path, jsxId) {
58 var jsxIdWithSpace = concat(jsxId, t.stringLiteral(' '));
59 var attributes = path.get('attributes');
60 var spreads = [];
61 var className = null; // Find className and collect spreads
62
63 for (var i = attributes.length - 1, attr; attr = attributes[i]; i--) {
64 var node = attr.node;
65
66 if (t.isJSXSpreadAttribute(attr)) {
67 if (t.isObjectExpression(node.argument)) {
68 var properties = node.argument.properties;
69 var index = properties.findIndex(function (property) {
70 return property.key.name === 'className';
71 });
72
73 if (~index) {
74 className = attr.get('argument').get("properties.".concat(index)); // Remove jsx spread attribute if there is only className property
75
76 if (properties.length === 1) {
77 attr.remove();
78 }
79
80 break;
81 }
82 }
83
84 if (t.isMemberExpression(node.argument) || t.isIdentifier(node.argument)) {
85 var name = node.argument.name;
86 var spreadObj = t.isMemberExpression(node.argument) ? node.argument : t.identifier(name);
87 var attrNameDotClassName = t.memberExpression(spreadObj, t.identifier('className'));
88 spreads.push( // `${name} && ${name}.className != null && ${name}.className`
89 and(spreadObj, and(t.binaryExpression('!=', attrNameDotClassName, t.nullLiteral()), attrNameDotClassName)));
90 }
91
92 continue;
93 }
94
95 if (t.isJSXAttribute(attr) && node.name.name === 'className') {
96 className = attributes[i]; // found className break the loop
97
98 break;
99 }
100 }
101
102 if (className) {
103 var newClassName = className.node.value.expression || className.node.value;
104 newClassName = t.isStringLiteral(newClassName) || t.isTemplateLiteral(newClassName) ? newClassName : or(newClassName, t.stringLiteral(''));
105 className.remove();
106 className = t.jSXExpressionContainer(spreads.length === 0 ? concat(jsxIdWithSpace, newClassName) : concat(jsxIdWithSpace, or(joinSpreads(spreads), newClassName)));
107 } else {
108 className = t.jSXExpressionContainer(spreads.length === 0 ? jsxId : concat(jsxIdWithSpace, or(joinSpreads(spreads), t.stringLiteral(''))));
109 }
110
111 path.node.attributes.push(t.jSXAttribute(t.jSXIdentifier('className'), className));
112};
113
114exports.addClassName = addClassName;
115
116var getScope = function getScope(path) {
117 return (path.findParent(function (path) {
118 return path.isFunctionDeclaration() || path.isArrowFunctionExpression() || path.isClassMethod();
119 }) || path).scope;
120};
121
122exports.getScope = getScope;
123
124var isGlobalEl = function isGlobalEl(el) {
125 return el && el.attributes.some(function (_ref) {
126 var name = _ref.name;
127 return name && name.name === _constants.GLOBAL_ATTRIBUTE;
128 });
129};
130
131exports.isGlobalEl = isGlobalEl;
132
133var isStyledJsx = function isStyledJsx(_ref2) {
134 var el = _ref2.node;
135 return t.isJSXElement(el) && el.openingElement.name.name === 'style' && el.openingElement.attributes.some(function (attr) {
136 return attr.name.name === _constants.STYLE_ATTRIBUTE;
137 });
138};
139
140exports.isStyledJsx = isStyledJsx;
141
142var findStyles = function findStyles(path) {
143 if (isStyledJsx(path)) {
144 var node = path.node;
145 return isGlobalEl(node.openingElement) ? [path] : [];
146 }
147
148 return path.get('children').filter(isStyledJsx);
149}; // The following visitor ensures that MemberExpressions and Identifiers
150// are not in the scope of the current Method (render) or function (Component).
151
152
153exports.findStyles = findStyles;
154var validateExpressionVisitor = {
155 MemberExpression: function MemberExpression(path, scope) {
156 var node = path.node;
157
158 if (t.isIdentifier(node.property) && t.isThisExpression(node.object) && (node.property.name === 'props' || node.property.name === 'state' || node.property.name === 'context') || t.isIdentifier(node.object) && scope.hasOwnBinding(node.object.name)) {
159 throw path.buildCodeFrameError("Expected a constant " + "as part of the template literal expression " + "(eg: <style jsx>{`p { color: ${myColor}`}</style>), " + "but got a MemberExpression: this.".concat(node.property.name));
160 }
161 },
162 Identifier: function Identifier(path, scope) {
163 var name = path.node.name;
164
165 if (t.isMemberExpression(path.parentPath) && scope.hasOwnBinding(name)) {
166 return;
167 }
168
169 var targetScope = path.scope;
170 var isDynamicBinding = false; // Traversing scope chain in order to find current variable.
171 // If variable has no parent scope and it's `const` then we can interp. it
172 // as static in order to optimize styles.
173 // `let` and `var` can be changed during runtime.
174
175 while (targetScope) {
176 if (targetScope.hasOwnBinding(name)) {
177 var binding = targetScope.bindings[name];
178 isDynamicBinding = binding.scope.parent !== null || binding.kind !== 'const';
179 break;
180 }
181
182 targetScope = targetScope.parent;
183 }
184
185 if (isDynamicBinding) {
186 throw path.buildCodeFrameError("Expected `".concat(name, "` ") + "to not come from the closest scope.\n" + "Styled JSX encourages the use of constants " + "instead of `props` or dynamic values " + "which are better set via inline styles or `className` toggling. " + "See https://github.com/zeit/styled-jsx#dynamic-styles");
187 }
188 }
189}; // Use `validateExpressionVisitor` to determine whether the `expr`ession has dynamic values.
190
191exports.validateExpressionVisitor = validateExpressionVisitor;
192
193var isDynamic = function isDynamic(expr, scope) {
194 try {
195 expr.traverse(validateExpressionVisitor, scope);
196 return false;
197 } catch (error) {}
198
199 return true;
200};
201
202exports.isDynamic = isDynamic;
203var validateExternalExpressionsVisitor = {
204 Identifier: function Identifier(path) {
205 if (t.isMemberExpression(path.parentPath)) {
206 return;
207 }
208
209 var name = path.node.name;
210
211 if (!path.scope.hasBinding(name)) {
212 throw path.buildCodeFrameError(path.getSource());
213 }
214 },
215 MemberExpression: function MemberExpression(path) {
216 var node = path.node;
217
218 if (!t.isIdentifier(node.object)) {
219 return;
220 }
221
222 if (!path.scope.hasBinding(node.object.name)) {
223 throw path.buildCodeFrameError(path.getSource());
224 }
225 },
226 ThisExpression: function ThisExpression(path) {
227 throw new Error(path.parentPath.getSource());
228 }
229};
230
231var validateExternalExpressions = function validateExternalExpressions(path) {
232 try {
233 path.traverse(validateExternalExpressionsVisitor);
234 } catch (error) {
235 throw path.buildCodeFrameError("\n Found an `undefined` or invalid value in your styles: `".concat(error.message, "`.\n\n If you are trying to use dynamic styles in external files this is unfortunately not possible yet.\n Please put the dynamic parts alongside the component. E.g.\n\n <button>\n <style jsx>{externalStylesReference}</style>\n <style jsx>{`\n button { background-color: ${").concat(error.message, "} }\n `}</style>\n </button>\n "));
236 }
237};
238
239exports.validateExternalExpressions = validateExternalExpressions;
240
241var getJSXStyleInfo = function getJSXStyleInfo(expr, scope) {
242 var node = expr.node;
243 var location = node.loc; // Assume string literal
244
245 if (t.isStringLiteral(node)) {
246 return {
247 hash: hashString(node.value),
248 css: node.value,
249 expressions: [],
250 dynamic: false,
251 location: location
252 };
253 } // Simple template literal without expressions
254
255
256 if (node.expressions.length === 0) {
257 return {
258 hash: hashString(node.quasis[0].value.raw),
259 css: node.quasis[0].value.raw,
260 expressions: [],
261 dynamic: false,
262 location: location
263 };
264 } // Special treatment for template literals that contain expressions:
265 //
266 // Expressions are replaced with a placeholder
267 // so that the CSS compiler can parse and
268 // transform the css source string
269 // without having to know about js literal expressions.
270 // Later expressions are restored.
271 //
272 // e.g.
273 // p { color: ${myConstant}; }
274 // becomes
275 // p { color: %%styled-jsx-placeholder-${id}%%; }
276
277
278 var quasis = node.quasis,
279 expressions = node.expressions;
280 var hash = hashString(expr.getSource().slice(1, -1));
281 var dynamic = scope ? isDynamic(expr, scope) : false;
282 var css = quasis.reduce(function (css, quasi, index) {
283 return "".concat(css).concat(quasi.value.raw).concat(quasis.length === index + 1 ? '' : "%%styled-jsx-placeholder-".concat(index, "%%"));
284 }, '');
285 return {
286 hash: hash,
287 css: css,
288 expressions: expressions,
289 dynamic: dynamic,
290 location: location
291 };
292};
293
294exports.getJSXStyleInfo = getJSXStyleInfo;
295
296var computeClassNames = function computeClassNames(styles, externalJsxId) {
297 if (styles.length === 0) {
298 return {
299 className: externalJsxId
300 };
301 }
302
303 var hashes = styles.reduce(function (acc, styles) {
304 if (styles.dynamic === false) {
305 acc.static.push(styles.hash);
306 } else {
307 acc.dynamic.push(styles);
308 }
309
310 return acc;
311 }, {
312 static: [],
313 dynamic: []
314 });
315 var staticClassName = "jsx-".concat(hashString(hashes.static.join(','))); // Static and optionally external classes. E.g.
316 // '[jsx-externalClasses] jsx-staticClasses'
317
318 if (hashes.dynamic.length === 0) {
319 return {
320 staticClassName: staticClassName,
321 className: externalJsxId ? concat(t.stringLiteral(staticClassName + ' '), externalJsxId) : t.stringLiteral(staticClassName)
322 };
323 } // _JSXStyle.dynamic([ ['1234', [props.foo, bar, fn(props)]], ... ])
324
325
326 var dynamic = t.callExpression( // Callee: _JSXStyle.dynamic
327 t.memberExpression(t.identifier(_constants.STYLE_COMPONENT), t.identifier('dynamic')), // Arguments
328 [t.arrayExpression(hashes.dynamic.map(function (styles) {
329 return t.arrayExpression([t.stringLiteral(hashString(styles.hash + staticClassName)), t.arrayExpression(styles.expressions)]);
330 }))]); // Dynamic and optionally external classes. E.g.
331 // '[jsx-externalClasses] ' + _JSXStyle.dynamic([ ['1234', [props.foo, bar, fn(props)]], ... ])
332
333 if (hashes.static.length === 0) {
334 return {
335 staticClassName: staticClassName,
336 className: externalJsxId ? concat(concat(externalJsxId, t.stringLiteral(' ')), dynamic) : dynamic
337 };
338 } // Static, dynamic and optionally external classes. E.g.
339 // '[jsx-externalClasses] jsx-staticClasses ' + _JSXStyle.dynamic([ ['5678', [props.foo, bar, fn(props)]], ... ])
340
341
342 return {
343 staticClassName: staticClassName,
344 className: externalJsxId ? concat(concat(externalJsxId, t.stringLiteral(" ".concat(staticClassName, " "))), dynamic) : concat(t.stringLiteral("".concat(staticClassName, " ")), dynamic)
345 };
346};
347
348exports.computeClassNames = computeClassNames;
349
350var templateLiteralFromPreprocessedCss = function templateLiteralFromPreprocessedCss(css, expressions) {
351 var quasis = [];
352 var finalExpressions = [];
353 var parts = css.split(/(?:%%styled-jsx-placeholder-(\d+)%%)/g);
354
355 if (parts.length === 1) {
356 return t.stringLiteral(css);
357 }
358
359 parts.forEach(function (part, index) {
360 if (index % 2 > 0) {
361 // This is necessary because, after preprocessing, declarations might have been alterate.
362 // eg. properties are auto prefixed and therefore expressions need to match.
363 finalExpressions.push(expressions[part]);
364 } else {
365 quasis.push(part);
366 }
367 });
368 return t.templateLiteral(quasis.map(function (quasi, index) {
369 return t.templateElement({
370 raw: quasi,
371 cooked: quasi
372 }, quasis.length === index + 1);
373 }), finalExpressions);
374};
375
376exports.templateLiteralFromPreprocessedCss = templateLiteralFromPreprocessedCss;
377
378var cssToBabelType = function cssToBabelType(css) {
379 if (typeof css === 'string') {
380 return t.stringLiteral(css);
381 }
382
383 if (Array.isArray(css)) {
384 return t.arrayExpression(css);
385 }
386
387 return t.cloneDeep(css);
388};
389
390exports.cssToBabelType = cssToBabelType;
391
392var makeStyledJsxTag = function makeStyledJsxTag(id, transformedCss) {
393 var expressions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
394 var css = cssToBabelType(transformedCss);
395 var attributes = [t.jSXAttribute(t.jSXIdentifier(_constants.STYLE_COMPONENT_ID), t.jSXExpressionContainer(typeof id === 'string' ? t.stringLiteral(id) : id))];
396
397 if (expressions.length > 0) {
398 attributes.push(t.jSXAttribute(t.jSXIdentifier(_constants.STYLE_COMPONENT_DYNAMIC), t.jSXExpressionContainer(t.arrayExpression(expressions))));
399 }
400
401 return t.jSXElement(t.jSXOpeningElement(t.jSXIdentifier(_constants.STYLE_COMPONENT), attributes), t.jSXClosingElement(t.jSXIdentifier(_constants.STYLE_COMPONENT)), [t.jSXExpressionContainer(css)]);
402};
403
404exports.makeStyledJsxTag = makeStyledJsxTag;
405
406var makeSourceMapGenerator = function makeSourceMapGenerator(file) {
407 var filename = file.sourceFileName;
408 var generator = new _sourceMap.SourceMapGenerator({
409 file: filename,
410 sourceRoot: file.sourceRoot
411 });
412 generator.setSourceContent(filename, file.code);
413 return generator;
414};
415
416exports.makeSourceMapGenerator = makeSourceMapGenerator;
417
418var addSourceMaps = function addSourceMaps(code, generator, filename) {
419 var sourceMaps = [_convertSourceMap.default.fromObject(generator).toComment({
420 multiline: true
421 }), "/*@ sourceURL=".concat(filename, " */")];
422
423 if (Array.isArray(code)) {
424 return code.concat(sourceMaps);
425 }
426
427 return [code].concat(sourceMaps).join('\n');
428};
429
430exports.addSourceMaps = addSourceMaps;
431var combinedPluginsCache = {
432 plugins: null,
433 combined: null
434};
435
436var combinePlugins = function combinePlugins(plugins) {
437 if (!plugins) {
438 return function (css) {
439 return css;
440 };
441 }
442
443 var pluginsToString = JSON.stringify(plugins);
444
445 if (combinedPluginsCache.plugins === pluginsToString) {
446 return combinedPluginsCache.combined;
447 }
448
449 if (!Array.isArray(plugins) || plugins.some(function (p) {
450 return !Array.isArray(p) && typeof p !== 'string';
451 })) {
452 throw new Error('`plugins` must be an array of plugins names (string) or an array `[plugin-name, {options}]`');
453 }
454
455 combinedPluginsCache.plugins = pluginsToString;
456 combinedPluginsCache.combined = plugins.map(function (plugin, i) {
457 var options = {};
458
459 if (Array.isArray(plugin)) {
460 options = plugin[1] || {};
461 plugin = plugin[0];
462
463 if (Object.prototype.hasOwnProperty.call(options, 'babel')) {
464 throw new Error("\n Error while trying to register the styled-jsx plugin: ".concat(plugin, "\n The option name `babel` is reserved.\n "));
465 }
466 }
467
468 log('Loading plugin from path: ' + plugin);
469
470 var p = require(plugin);
471
472 if (p.default) {
473 p = p.default;
474 }
475
476 var type = _typeof(p);
477
478 if (type !== 'function') {
479 throw new Error("Expected plugin ".concat(plugins[i], " to be a function but instead got ").concat(type));
480 }
481
482 return {
483 plugin: p,
484 options: options
485 };
486 }).reduce(function (previous, _ref3) {
487 var plugin = _ref3.plugin,
488 options = _ref3.options;
489 return function (css, babelOptions) {
490 return plugin(previous ? previous(css, babelOptions) : css, _objectSpread({}, options, {
491 babel: babelOptions
492 }));
493 };
494 }, null);
495 return combinedPluginsCache.combined;
496};
497
498exports.combinePlugins = combinePlugins;
499
500var getPrefix = function getPrefix(isDynamic, id) {
501 return isDynamic ? '.__jsx-style-dynamic-selector' : ".".concat(id);
502};
503
504var processCss = function processCss(stylesInfo, options) {
505 var hash = stylesInfo.hash,
506 css = stylesInfo.css,
507 expressions = stylesInfo.expressions,
508 dynamic = stylesInfo.dynamic,
509 location = stylesInfo.location,
510 file = stylesInfo.file,
511 isGlobal = stylesInfo.isGlobal,
512 plugins = stylesInfo.plugins,
513 vendorPrefixes = stylesInfo.vendorPrefixes,
514 sourceMaps = stylesInfo.sourceMaps;
515 var fileInfo = {
516 code: file.code,
517 sourceRoot: file.opts.sourceRoot,
518 filename: file.opts.filename || file.filename
519 };
520 fileInfo.sourceFileName = file.opts.sourceFileName || file.sourceFileName || // According to https://babeljs.io/docs/en/options#source-map-options
521 // filenameRelative = path.relative(file.opts.cwd, file.opts.filename)
522 // sourceFileName = path.basename(filenameRelative)
523 // or simply
524 // sourceFileName = path.basename(file.opts.filename)
525 fileInfo.filename && _path.default.basename(fileInfo.filename);
526 var staticClassName = stylesInfo.staticClassName || "jsx-".concat(hashString(hash));
527 var splitRules = options.splitRules;
528 var useSourceMaps = Boolean(sourceMaps) && !splitRules;
529 var pluginsOptions = {
530 location: {
531 start: _objectSpread({}, location.start),
532 end: _objectSpread({}, location.end)
533 },
534 vendorPrefixes: vendorPrefixes,
535 sourceMaps: useSourceMaps,
536 isGlobal: isGlobal,
537 filename: fileInfo.filename
538 };
539 var transformedCss;
540
541 if (useSourceMaps) {
542 var generator = makeSourceMapGenerator(fileInfo);
543 var filename = fileInfo.sourceFileName;
544 transformedCss = addSourceMaps((0, _styleTransform.default)(isGlobal ? '' : getPrefix(dynamic, staticClassName), plugins(css, pluginsOptions), {
545 generator: generator,
546 offset: location.start,
547 filename: filename,
548 splitRules: splitRules,
549 vendorPrefixes: vendorPrefixes
550 }), generator, filename);
551 } else {
552 transformedCss = (0, _styleTransform.default)(isGlobal ? '' : getPrefix(dynamic, staticClassName), plugins(css, pluginsOptions), {
553 splitRules: splitRules,
554 vendorPrefixes: vendorPrefixes
555 });
556 }
557
558 if (expressions.length > 0) {
559 if (typeof transformedCss === 'string') {
560 transformedCss = templateLiteralFromPreprocessedCss(transformedCss, expressions);
561 } else {
562 transformedCss = transformedCss.map(function (transformedCss) {
563 return templateLiteralFromPreprocessedCss(transformedCss, expressions);
564 });
565 }
566 } else if (Array.isArray(transformedCss)) {
567 transformedCss = transformedCss.map(function (transformedCss) {
568 return t.stringLiteral(transformedCss);
569 });
570 }
571
572 return {
573 hash: dynamic ? hashString(hash + staticClassName) : hashString(hash),
574 css: transformedCss,
575 expressions: dynamic && expressions
576 };
577};
578
579exports.processCss = processCss;
580
581var booleanOption = function booleanOption(opts) {
582 var ret;
583 opts.some(function (opt) {
584 if (typeof opt === 'boolean') {
585 ret = opt;
586 return true;
587 }
588
589 return false;
590 });
591 return ret;
592};
593
594exports.booleanOption = booleanOption;
595
596var createReactComponentImportDeclaration = function createReactComponentImportDeclaration() {
597 return t.importDeclaration([t.importDefaultSpecifier(t.identifier(_constants.STYLE_COMPONENT))], t.stringLiteral('styled-jsx/style'));
598};
599
600exports.createReactComponentImportDeclaration = createReactComponentImportDeclaration;
601
602var setStateOptions = function setStateOptions(state) {
603 var vendorPrefixes = booleanOption([state.opts.vendorPrefixes, state.file.opts.vendorPrefixes]);
604 state.opts.vendorPrefixes = typeof vendorPrefixes === 'boolean' ? vendorPrefixes : true;
605 var sourceMaps = booleanOption([state.opts.sourceMaps, state.file.opts.sourceMaps]);
606 state.opts.sourceMaps = Boolean(sourceMaps);
607
608 if (!state.plugins) {
609 state.plugins = combinePlugins(state.opts.plugins, {
610 sourceMaps: state.opts.sourceMaps,
611 vendorPrefixes: state.opts.vendorPrefixes
612 });
613 }
614};
615
616exports.setStateOptions = setStateOptions;
617
618function log(message) {
619 console.log('[styled-jsx] ' + message);
620}
\No newline at end of file