Skip to content

Commit

Permalink
Remove Blaze dependencies from static-html (meteor#10267)
Browse files Browse the repository at this point in the history
These changes create a new copy of the static-html and
caching-html-compiler packages in core, as well as a new package
called html-scanner, to house the html-scanner.js functionality
from the templating-tools package. With these changes in place,
we're able to remove all Blaze dependencies from static-html,
which benefits React based Meteor apps.

We don't need the extra `CompileError` class, and using it
was throwing off `caching-html-compiler` error handling.
Errors with messages and line numbers weren't being
interpreted / formatted properly.
  • Loading branch information
hwillson authored and benjamn committed Nov 23, 2018
1 parent f4ebac5 commit 4aad077
Show file tree
Hide file tree
Showing 11 changed files with 804 additions and 9 deletions.
22 changes: 13 additions & 9 deletions History.md
Expand Up @@ -3,11 +3,15 @@
### Breaking changes
N/A

### Migration steps
### Migration Steps
N/A

### Changes

* The `static-html` package is now part of the Meteor core, and no longer has
any dependencies on Blaze templating tools.
[PR #10267](https://github.com/meteor/meteor/pull/10267)

## v1.8.0.1, 2018-11-23

### Breaking changes
Expand Down Expand Up @@ -419,7 +423,7 @@ N/A

### Migration Steps

* Update `@babel/runtime` (as well as other Babel-related packages) and
* Update `@babel/runtime` (as well as other Babel-related packages) and
`meteor-node-stubs` to their latest versions:
```sh
meteor npm install @babel/runtime@latest meteor-node-stubs@latest
Expand Down Expand Up @@ -804,7 +808,7 @@ N/A
N/A

### Migration Steps
* Update `@babel/runtime` npm package and any custom Babel plugin enabled in
* Update `@babel/runtime` npm package and any custom Babel plugin enabled in
`.babelrc`
```sh
meteor npm install @babel/runtime@latest
Expand Down Expand Up @@ -839,7 +843,7 @@ N/A
values are not first converted to `null`, when inserted/updated. `undefined`
values are now removed from all Mongo queries and insert/update documents.

This is a potentially breaking change if you are upgrading an existing app
This is a potentially breaking change if you are upgrading an existing app
from an earlier version of Meteor.

For example:
Expand All @@ -849,11 +853,11 @@ N/A
userId: currentUser._id // undefined
});
```
Assuming there are no documents in the `privateUserData` collection with
`userId: null`, in Meteor versions prior to 1.6.1 this query will return
zero documents. From Meteor 1.6.1 onwards, this query will now return
_every_ document in the collection. It is highly recommend you review all
your existing queries to ensure that any potential usage of `undefined` in
Assuming there are no documents in the `privateUserData` collection with
`userId: null`, in Meteor versions prior to 1.6.1 this query will return
zero documents. From Meteor 1.6.1 onwards, this query will now return
_every_ document in the collection. It is highly recommend you review all
your existing queries to ensure that any potential usage of `undefined` in
query objects won't lead to problems.

### Migration Steps
Expand Down
49 changes: 49 additions & 0 deletions packages/caching-html-compiler/README.md
@@ -0,0 +1,49 @@
# caching-html-compiler

Provides a pluggable class used to compile HTML-style templates in Meteor build
plugins. This abstracts out a lot of the functionality you would need to
implement the following plugins:

1. `templating`
2. `static-html`
3. `simple:markdown-templating`

It provides automatic caching and handles communicating with the build plugin
APIs. The actual functions that convert HTML into compiled form are passed in
as arguments into the constructor, allowing those functions to be unit tested
separately from the caching and file system functionality.

-------

### new CachingHtmlCompiler(name, tagScannerFunc, tagHandlerFunc)

Constructs a new CachingHtmlCompiler that can be passed into
`Plugin.registerCompiler`.

#### Arguments

1. `name` The name of the compiler, used when printing errors. Should probably
be the same as the name of the build plugin and package it is used in.
2. `tagScannerFunc` A function that takes a string representing a template
file as input, and returns an array of Tag objects. See the README for
`templating-tools` for more information about the Tag object.
3. `tagHandlerFunc` A function that takes an array of Tag objects (the output
of the previous argument) and returns an object with `js`, `body`, `head`,
and `bodyAttr` properties, which will be added to the app through the build
plugin API.

#### Example

Here is some example code from the `templating` package:

```js
Plugin.registerCompiler({
extensions: ['html'],
archMatching: 'web',
isTemplate: true
}, () => new CachingHtmlCompiler(
"templating",
TemplatingTools.scanHtmlForTags,
TemplatingTools.compileTagsWithSpacebars
));
```
147 changes: 147 additions & 0 deletions packages/caching-html-compiler/caching-html-compiler.js
@@ -0,0 +1,147 @@
const path = Plugin.path;

// The CompileResult type for this CachingCompiler is the return value of
// htmlScanner.scan: a {js, head, body, bodyAttrs} object.
CachingHtmlCompiler = class CachingHtmlCompiler extends CachingCompiler {
/**
* Constructor for CachingHtmlCompiler
* @param {String} name The name of the compiler, printed in errors -
* should probably always be the same as the name of the build
* plugin/package
* @param {Function} tagScannerFunc Transforms a template file (commonly
* .html) into an array of Tags
* @param {Function} tagHandlerFunc Transforms an array of tags into a
* results object with js, body, head, and bodyAttrs properties
*/
constructor(name, tagScannerFunc, tagHandlerFunc) {
super({
compilerName: name,
defaultCacheSize: 1024*1024*10,
});

this._bodyAttrInfo = null;

this.tagScannerFunc = tagScannerFunc;
this.tagHandlerFunc = tagHandlerFunc;
}

// Implements method from CachingCompilerBase
compileResultSize(compileResult) {
function lengthOrZero(field) {
return field ? field.length : 0;
}
return lengthOrZero(compileResult.head) + lengthOrZero(compileResult.body) +
lengthOrZero(compileResult.js);
}

// Overrides method from CachingCompiler
processFilesForTarget(inputFiles) {
this._bodyAttrInfo = {};
super.processFilesForTarget(inputFiles);
}

// Implements method from CachingCompilerBase
getCacheKey(inputFile) {
// Note: the path is only used for errors, so it doesn't have to be part
// of the cache key.
return inputFile.getSourceHash();
}

// Implements method from CachingCompiler
compileOneFile(inputFile) {
const contents = inputFile.getContentsAsString();
const inputPath = inputFile.getPathInPackage();

// Since we can't control node_modules based HTML content, we'll skip
// over it here to avoid trying to handle HTML files that don't
// fit within Meteor's HTML file handling rules (e.g. Meteor doesn't
// allow files that have a DOCTYPE specified, since it adds its own).
if (!inputPath.startsWith('node_modules')) {
try {
const tags = this.tagScannerFunc({
sourceName: inputPath,
contents: contents,
tagNames: ["body", "head", "template"]
});
return this.tagHandlerFunc(tags);
} catch (e) {
if (e.message && e.line) {
inputFile.error({
message: e.message,
line: e.line
});
return null;
} else {
throw e;
}
}
}
}

// Implements method from CachingCompilerBase
addCompileResult(inputFile, compileResult) {
let allJavaScript = "";

if (compileResult.head) {
inputFile.addHtml({ section: "head", data: compileResult.head });
}

if (compileResult.body) {
inputFile.addHtml({ section: "body", data: compileResult.body });
}

if (compileResult.js) {
allJavaScript += compileResult.js;
}

if (Object.keys(compileResult.bodyAttrs).length !== 0) {
Object.keys(compileResult.bodyAttrs).forEach((attr) => {
const value = compileResult.bodyAttrs[attr];
if (this._bodyAttrInfo.hasOwnProperty(attr) &&
this._bodyAttrInfo[attr].value !== value) {
// two conflicting attributes on <body> tags in two different template
// files
inputFile.error({
message:
`<body> declarations have conflicting values for the '${ attr }' ` +
`attribute in the following files: ` +
this._bodyAttrInfo[attr].inputFile.getPathInPackage() +
`, ${ inputFile.getPathInPackage() }`
});
} else {
this._bodyAttrInfo[attr] = {inputFile, value};
}
});

// Add JavaScript code to set attributes on body
allJavaScript +=
`Meteor.startup(function() {
var attrs = ${JSON.stringify(compileResult.bodyAttrs)};
for (var prop in attrs) {
document.body.setAttribute(prop, attrs[prop]);
}
});
`;
}


if (allJavaScript) {
const filePath = inputFile.getPathInPackage();
// XXX this path manipulation may be unnecessarily complex
let pathPart = path.dirname(filePath);
if (pathPart === '.')
pathPart = '';
if (pathPart.length && pathPart !== path.sep)
pathPart = pathPart + path.sep;
const ext = path.extname(filePath);
const basename = path.basename(filePath, ext);

// XXX generate a source map

inputFile.addJavaScript({
path: path.join(pathPart, "template." + basename + ".js"),
data: allJavaScript
});
}
}
}
19 changes: 19 additions & 0 deletions packages/caching-html-compiler/package.js
@@ -0,0 +1,19 @@
Package.describe({
name: 'caching-html-compiler',
summary: "Pluggable class for compiling HTML into templates",
version: '1.1.3',
git: 'https://github.com/meteor/meteor.git'
});

Package.onUse(function (api) {
api.use([
'caching-compiler',
'ecmascript'
]);

api.export('CachingHtmlCompiler', 'server');

api.addFiles([
'caching-html-compiler.js'
], 'server');
});
54 changes: 54 additions & 0 deletions packages/html-scanner/README.md
@@ -0,0 +1,54 @@
# html-scanner

## `scanHtmlForTags(options)`

Scan an HTML file for top-level tags as specified by `options.tagNames`, and
return an array of `Tag` objects.

### Options

1. `sourceName` the name of the input file, used when throwing errors.
2. `contents` the contents of the input file, these are parsed to find the
top-level tags.
3. `tagNames` the top-level tags to look for in the HTML.

### Example

```js
const tags = scanHtmlForTags({
sourceName: inputPath,
contents: contents,
tagNames: ["body", "head", "template"]
});
```

### Tag object

```js
{
// Name of the tag - "body", "head", "template", etc
tagName: String,

// Attributes on the tag
attribs: { [attrName]: String },

// Contents of the tag
contents: String,

// Starting index of the opening tag in the source file
// (used to throw informative errors)
tagStartIndex: Number,

// Starting index of the contents of the tag in the source file
// (used to throw informative errors)
contentsStartIndex: Number,

// The contents of the entire source file, should be used only to
// throw informative errors (for example, this can be used to
// determine the line number for an error)
fileContents: String,

// The file name of the initial source file, used to throw errors
sourceName: String
};
```

0 comments on commit 4aad077

Please sign in to comment.