diff --git a/example/package.json b/example/package.json
index 34b696b..e6c2851 100644
--- a/example/package.json
+++ b/example/package.json
@@ -4,7 +4,8 @@
"main": "index.js",
"license": "MIT",
"scripts": {
- "start": "webpack-dev-server"
+ "start": "webpack-dev-server",
+ "build": "webpack --mode development"
},
"dependencies": {
"react": "^16.13.1",
diff --git a/example/src/App.js b/example/src/App.js
index faf1065..5b39b1b 100644
--- a/example/src/App.js
+++ b/example/src/App.js
@@ -3,13 +3,23 @@ import stylemug from 'stylemug';
import { globalStyles } from './globals';
const styles = stylemug.create({
+ foo: 'bar',
title: {
fontSize: '31px',
fontFamily: 'courier',
- color: '#444',
+ color: 'green',
},
titleRed: {
color: 'red',
+ '&:hoverr': {
+ color: 'red',
+ },
+ },
+});
+
+const secondaryStyles = stylemug.create({
+ color: {
+ color: '#444',
},
});
@@ -22,7 +32,11 @@ export default function App() {
return (
-
Hello World
+
+ Hello World
+
);
diff --git a/packages/babel-stylemug-plugin/src/__test__/__snapshots__/babel.test.js.snap b/packages/babel-stylemug-plugin/src/__test__/__snapshots__/babel.test.js.snap
index 25c692f..b4e37d3 100644
--- a/packages/babel-stylemug-plugin/src/__test__/__snapshots__/babel.test.js.snap
+++ b/packages/babel-stylemug-plugin/src/__test__/__snapshots__/babel.test.js.snap
@@ -1,5 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`babel plugin - reports should replace create argument 1`] = `
+"\\"use strict\\";
+
+var _stylemug = _interopRequireDefault(require(\\"stylemug\\"));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \\"default\\": obj }; }
+
+var styles = _stylemug[\\"default\\"].create({}, {
+ sourceLinesRange: \\"In lines 4 to 4\\",
+ messages: [\\"mockError1\\", \\"mockError2\\"]
+});"
+`;
+
exports[`babel plugin should error when failed to resolve 1`] = `
"\\"use strict\\";
@@ -13,7 +26,10 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
var styles = _stylemug[\\"default\\"].create(_defineProperty({}, _mock.selector, {
color: 'red'
-}), \\"Failed to evaluate the following stylesheet: \\\\n\\\\nstylemug.create({\\\\n [selector]: {\\\\n color: 'red'\\\\n }\\\\n})\\\\n\\\\nMake sure your stylesheet is statically defined.\\");"
+}), {
+ sourceLinesRange: \\"In lines 5 to 9\\",
+ messages: [\\"Failed to evaluate the following stylesheet: \\\\n\\\\nstylemug.create({\\\\n [selector]: {\\\\n color: 'red'\\\\n }\\\\n})\\\\n\\\\nMake sure your stylesheet is statically defined.\\"]
+});"
`;
exports[`babel plugin should replace create argument 1`] = `
diff --git a/packages/babel-stylemug-plugin/src/__test__/babel.test.js b/packages/babel-stylemug-plugin/src/__test__/babel.test.js
index 320457a..cfe6b16 100644
--- a/packages/babel-stylemug-plugin/src/__test__/babel.test.js
+++ b/packages/babel-stylemug-plugin/src/__test__/babel.test.js
@@ -1,17 +1,25 @@
import { transform } from '@babel/core';
+import { compileSchema } from 'stylemug-compiler';
import { babelPlugin as plugin } from '../babel';
jest.mock('stylemug-compiler', () => ({
- compileSchema: () => ({
- className: {
- hash: {
- keyId: 'id',
- },
- },
- }),
+ compileSchema: jest.fn(),
}));
describe('babel plugin', () => {
+ beforeEach(() => {
+ compileSchema.mockImplementationOnce(() => ({
+ result: {
+ className: {
+ hash: {
+ keyId: 'id',
+ },
+ },
+ },
+ reports: [],
+ }));
+ });
+
it('should replace create argument', () => {
const example = `
import stylemug from 'stylemug';
@@ -73,3 +81,24 @@ describe('babel plugin', () => {
expect(code).toMatchSnapshot();
});
});
+
+describe('babel plugin - reports', () => {
+ it('should replace create argument', () => {
+ compileSchema.mockImplementationOnce(() => ({
+ result: {},
+ reports: [{ message: 'mockError1' }, { message: 'mockError2' }],
+ }));
+
+ const example = `
+ import stylemug from 'stylemug';
+
+ const styles = stylemug.create({});
+ `;
+
+ const { code } = transform(example, {
+ plugins: [plugin],
+ });
+
+ expect(code).toMatchSnapshot();
+ });
+});
diff --git a/packages/babel-stylemug-plugin/src/babel.js b/packages/babel-stylemug-plugin/src/babel.js
index 46722c6..a284ff9 100644
--- a/packages/babel-stylemug-plugin/src/babel.js
+++ b/packages/babel-stylemug-plugin/src/babel.js
@@ -5,9 +5,39 @@ import { compileSchema } from 'stylemug-compiler';
export function babelPlugin(babel) {
const t = babel.types;
- function defineError(path, msg) {
+ function wrapReports(path, reports) {
+ if (!Array.isArray(reports)) {
+ reports = [reports];
+ }
+ if (!reports.length) {
+ return;
+ }
+
const node = t.cloneDeep(path.node);
- node.arguments[1] = t.stringLiteral(msg);
+ const objectChilds = [
+ t.objectProperty(
+ t.identifier('sourceLinesRange'),
+ t.stringLiteral(
+ 'In lines ' + node.loc.start.line + ' to ' + node.loc.end.line
+ )
+ ),
+ t.objectProperty(
+ t.identifier('messages'),
+ t.arrayExpression(
+ reports.map((report) => t.stringLiteral(report.message))
+ )
+ ),
+ ];
+ if (path.hub.file.opts.filename) {
+ objectChilds.push(
+ t.objectProperty(
+ t.identifier('fileName'),
+ t.stringLiteral(path.hub.file.opts.filename || 'unknown')
+ )
+ );
+ }
+
+ node.arguments[1] = t.objectExpression(objectChilds);
path.replaceWith(node);
}
@@ -34,22 +64,24 @@ export function babelPlugin(babel) {
references.forEach((reference) => {
const local = reference.parentPath.parentPath;
- let sheet = evaluateSimple(local.get('arguments')[0]);
+ const sheet = evaluateSimple(local.get('arguments')[0]);
if (!sheet.confident) {
- defineError(
- local,
- 'Failed to evaluate the following stylesheet: \n\n' +
+ wrapReports(local, {
+ message:
+ 'Failed to evaluate the following stylesheet: \n\n' +
local.toString() +
'\n\n' +
- 'Make sure your stylesheet is statically defined.'
- );
+ 'Make sure your stylesheet is statically defined.',
+ });
return;
}
- sheet = compileSchema(sheet.value);
+
+ const { result, reports } = compileSchema(sheet.value);
+ wrapReports(local, reports);
const nextLocal = t.cloneDeep(local.node);
nextLocal.arguments[0] = t.objectExpression(
- Object.entries(sheet).map(([name, rules]) => {
+ Object.entries(result).map(([name, rules]) => {
return t.objectProperty(
t.identifier(name),
t.objectExpression(
diff --git a/packages/stylemug-compiler/src/__test__/__snapshots__/compile-context.test.js.snap b/packages/stylemug-compiler/src/__test__/__snapshots__/compile-context.test.js.snap
new file mode 100644
index 0000000..ee132ae
--- /dev/null
+++ b/packages/stylemug-compiler/src/__test__/__snapshots__/compile-context.test.js.snap
@@ -0,0 +1,63 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`compile should assert an invalid rule value 1`] = `
+Array [
+ Object {
+ "message": "Invalid type for color",
+ },
+]
+`;
+
+exports[`compile should assert an invalid sheet 1`] = `
+Object {
+ "default": Object {
+ "b765dfa68": Object {
+ "children": "",
+ "key": "backgroundColor",
+ "keyId": "bb7c5a6d0",
+ "media": undefined,
+ "value": "yellow",
+ },
+ "cb34cd11f": Object {
+ "children": "",
+ "key": "color",
+ "keyId": "c3d7e6258",
+ "media": undefined,
+ "value": "red",
+ },
+ },
+}
+`;
+
+exports[`compile should assert an invalid sheet 2`] = `
+Array [
+ Object {
+ "message": "Classname with key foo is not an object",
+ },
+]
+`;
+
+exports[`compile should assert invalid pseudo classes 1`] = `
+Array [
+ Object {
+ "message": "&:iAmAnInvalidPseudoClass is an invalid CSS pseudo class / element",
+ },
+ Object {
+ "message": "Invalid type for color",
+ },
+]
+`;
+
+exports[`compile should report multiple errors 1`] = `
+Array [
+ Object {
+ "message": "Classname with key foo is not an object",
+ },
+ Object {
+ "message": "&:iAmAnInvalidPseudoClass is an invalid CSS pseudo class / element",
+ },
+ Object {
+ "message": "Invalid type for color",
+ },
+]
+`;
diff --git a/packages/stylemug-compiler/src/__test__/compile-context.test.js b/packages/stylemug-compiler/src/__test__/compile-context.test.js
new file mode 100644
index 0000000..ceddd11
--- /dev/null
+++ b/packages/stylemug-compiler/src/__test__/compile-context.test.js
@@ -0,0 +1,52 @@
+import { compileSchema } from '../compile';
+
+describe('compile', () => {
+ it('should assert an invalid sheet', () => {
+ const { result, reports } = compileSchema({
+ default: {
+ color: 'red',
+ backgroundColor: 'yellow',
+ },
+ // The rule below fails.
+ foo: 'bar',
+ });
+
+ expect(result).toMatchSnapshot();
+ expect(reports).toMatchSnapshot();
+ });
+
+ it('should assert an invalid rule value', () => {
+ const { reports } = compileSchema({
+ default: {
+ color: true,
+ },
+ });
+
+ expect(reports).toMatchSnapshot();
+ });
+
+ it('should assert invalid pseudo classes', () => {
+ const { reports } = compileSchema({
+ default: {
+ '&:iAmAnInvalidPseudoClass': {
+ color: true,
+ },
+ },
+ });
+
+ expect(reports).toMatchSnapshot();
+ });
+
+ it('should report multiple errors', () => {
+ const { reports } = compileSchema({
+ foo: 'bar',
+ default: {
+ '&:iAmAnInvalidPseudoClass': {
+ color: true,
+ },
+ },
+ });
+
+ expect(reports).toMatchSnapshot();
+ });
+});
diff --git a/packages/stylemug-compiler/src/__test__/compile.test.js b/packages/stylemug-compiler/src/__test__/compile.test.js
index 4c5c775..e001048 100644
--- a/packages/stylemug-compiler/src/__test__/compile.test.js
+++ b/packages/stylemug-compiler/src/__test__/compile.test.js
@@ -2,7 +2,7 @@ import { compileSchema, compileSelectors } from '../compile';
describe('compile', () => {
it('should compile schema', () => {
- const result = compileSchema({
+ const { result } = compileSchema({
default: {
color: 'red',
backgroundColor: 'yellow',
@@ -18,16 +18,8 @@ describe('compile', () => {
expect(result).toMatchSnapshot();
});
- it('should compile selectors with number as value', () => {
- const result = compileSelectors({
- fontSize: 12,
- });
-
- expect(result).toMatchSnapshot();
- });
-
it('should compile nested selectors', () => {
- const result = compileSchema({
+ const { result } = compileSchema({
default: {
color: 'red',
@@ -41,7 +33,7 @@ describe('compile', () => {
});
it('should compile nested media query', () => {
- const result = compileSchema({
+ const { result } = compileSchema({
default: {
color: 'red',
@@ -53,4 +45,11 @@ describe('compile', () => {
expect(result).toMatchSnapshot();
});
+
+ it('should compile selectors with number as value', () => {
+ const result = compileSelectors({
+ fontSize: 12,
+ });
+ expect(result).toMatchSnapshot();
+ });
});
diff --git a/packages/stylemug-compiler/src/compile.js b/packages/stylemug-compiler/src/compile.js
index 869f88c..542cf4b 100644
--- a/packages/stylemug-compiler/src/compile.js
+++ b/packages/stylemug-compiler/src/compile.js
@@ -1,6 +1,8 @@
import fnv1a from 'fnv1a';
import { save as extractorSave } from './extractor';
import { extractShorthands } from './shorthands';
+import { createContext } from './context';
+import { PseudoClasses, PseudoElements } from './static';
function hash(str) {
let hash = fnv1a(str).toString(16);
@@ -9,54 +11,70 @@ function hash(str) {
return `${prepend}${hash}`;
}
-export function compileSelectors(selectors, children, media) {
+export function compileSelectors(selectors, children, media, context) {
let result = {};
selectors = extractShorthands(selectors);
for (let key in selectors) {
const value = selectors[key];
+ const type = typeof value;
- switch (typeof value) {
- case 'object':
- // @media
- if (/^@/.test(key)) {
- result = Object.assign(
- result,
- compileSelectors(value, children, key)
- );
- }
- // &:focus
- else {
- const child = key.replace(/&/g, '');
- result = Object.assign(
- result,
- compileSelectors(value, children + child, media)
- );
- }
+ // @media
+ if (/^@/.test(key)) {
+ if (type !== 'object') {
+ context.report('Rule with key ' + key + ' is not an object');
continue;
+ }
+ result = Object.assign(
+ result,
+ compileSelectors(value, children, key, context)
+ );
+ continue;
+ }
+
+ // &:focus
+ // &::after
+ if (/^&/.test(key)) {
+ const child = key.replace(/&/g, '');
- case 'number':
- case 'string':
- const mediaKey = media || '';
- const className = hash(key + value + children + mediaKey);
-
- const entry = {
- // Provide a key id to match selectors setting the same key,
- // for example, when for a given key, two classes apply with both
- // backgroundColor as propery, we can filter out the first one by specificity.
- keyId: hash(key + children + mediaKey),
- key,
- value,
- children,
- media,
- };
-
- extractorSave(className, entry);
-
- result[className] = entry;
+ if (!PseudoClasses.includes(child) && !PseudoElements.includes(child)) {
+ context.report(key + ' is an invalid CSS pseudo class / element');
+ }
+ if (type !== 'object') {
+ context.report('Rule with key ' + key + ' is not an object');
continue;
+ }
+
+ result = Object.assign(
+ result,
+ compileSelectors(value, children + child, media, context)
+ );
+ continue;
+ }
+
+ if (type === 'number' || type === 'string') {
+ const mediaKey = media || '';
+ const className = hash(key + value + children + mediaKey);
+
+ const entry = {
+ // Provide a key id to match selectors setting the same key,
+ // for example, when for a given key, two classes apply with both
+ // backgroundColor as propery, we can filter out the first one by specificity.
+ keyId: hash(key + children + mediaKey),
+ key,
+ value,
+ children,
+ media,
+ };
+
+ extractorSave(className, entry);
+
+ result[className] = entry;
+ continue;
}
+
+ context.report('Invalid type for ' + key);
}
return result;
@@ -64,10 +82,16 @@ export function compileSelectors(selectors, children, media) {
export function compileSchema(schema) {
const result = {};
+ const context = createContext();
for (let key in schema) {
- result[key] = compileSelectors(schema[key], '');
+ if (typeof schema[key] !== 'object') {
+ context.report('Classname with key ' + key + ' is not an object');
+ continue;
+ }
+
+ result[key] = compileSelectors(schema[key], '', undefined, context);
}
- return result;
+ return { result, reports: context.getReports() };
}
diff --git a/packages/stylemug-compiler/src/context.js b/packages/stylemug-compiler/src/context.js
new file mode 100644
index 0000000..c813de5
--- /dev/null
+++ b/packages/stylemug-compiler/src/context.js
@@ -0,0 +1,18 @@
+export function createContext() {
+ const reports = [];
+
+ function report(message) {
+ reports.push({
+ message,
+ });
+ }
+
+ function getReports() {
+ return reports;
+ }
+
+ return {
+ report,
+ getReports,
+ };
+}
diff --git a/packages/stylemug-compiler/src/static.js b/packages/stylemug-compiler/src/static.js
new file mode 100644
index 0000000..93c8d6d
--- /dev/null
+++ b/packages/stylemug-compiler/src/static.js
@@ -0,0 +1,41 @@
+export const PseudoClasses = [
+ ':active',
+ ':checked',
+ ':disabled',
+ ':empty',
+ ':enabled',
+ ':first-child',
+ ':first-of-type',
+ ':focus',
+ ':hover',
+ ':in-range',
+ ':invalid',
+ ':lang(language)',
+ ':last-child',
+ ':last-of-type',
+ ':link',
+ ':not(selector)',
+ ':nth-child(n)',
+ ':nth-last-child(n)',
+ ':nth-last-of-type(n)',
+ ':nth-of-type(n)',
+ ':only-of-type',
+ ':only-child',
+ ':optional',
+ ':out-of-range',
+ ':read-only',
+ ':read-write',
+ ':required',
+ ':root',
+ ':target',
+ ':valid',
+ ':visited',
+];
+
+export const PseudoElements = [
+ '::after',
+ '::before',
+ '::first-letter',
+ '::first-line',
+ '::selection',
+];
diff --git a/packages/stylemug/src/__test__/runtime.test.js b/packages/stylemug/src/__test__/runtime.test.js
index a101e40..b3fd21b 100644
--- a/packages/stylemug/src/__test__/runtime.test.js
+++ b/packages/stylemug/src/__test__/runtime.test.js
@@ -43,21 +43,6 @@ describe('runtime', () => {
});
});
- describe('resolver warnings', () => {
- it('should warn when a lookup failed', () => {
- const styles = runtime.create({
- foo: {
- color: 'red',
- },
- });
- styles('unknown');
-
- expect(warn).toHaveBeenCalledWith(
- 'The class name "unknown" does not exist in your stylesheet. Check your stylemug.create({}) definition.'
- );
- });
- });
-
describe('compiler warnings', () => {
it('should warn if a compiler error is thrown', () => {
runtime.create(
diff --git a/packages/stylemug/src/__test__/warn.test.js b/packages/stylemug/src/__test__/warn.test.js
deleted file mode 100644
index 3a6fb74..0000000
--- a/packages/stylemug/src/__test__/warn.test.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import warn from '../warn';
-
-global.console.warn = jest.fn();
-
-it('should log a warning message', () => {
- warn('Something bad happened');
- expect(global.console.warn).toHaveBeenCalledWith(
- '[stylemug] Something bad happened'
- );
-});
diff --git a/packages/stylemug/src/index.js b/packages/stylemug/src/index.js
index 935ec74..daa7c9d 100644
--- a/packages/stylemug/src/index.js
+++ b/packages/stylemug/src/index.js
@@ -1,14 +1,9 @@
import warn from './warn';
-const noop = () => {};
-
export default {
create(schema, error) {
- if (error) {
- if (__DEV__) {
- warn(error);
- }
- return noop;
+ if (__DEV__ && error) {
+ warn(error);
}
const resolver = (...classNames) => {
@@ -24,14 +19,6 @@ export default {
maps.push(className);
}
if (typeof className === 'string') {
- if (__DEV__ && !schema[className]) {
- warn(
- 'The class name "' +
- className +
- '" does not exist in your stylesheet. Check your ' +
- 'stylemug.create({}) definition.'
- );
- }
maps.push(schema[className]);
}
}
diff --git a/packages/stylemug/src/warn.js b/packages/stylemug/src/warn.js
index ec6e9eb..69ce069 100644
--- a/packages/stylemug/src/warn.js
+++ b/packages/stylemug/src/warn.js
@@ -1,9 +1,15 @@
-const warnings = {};
+export default function warn(error) {
+ let message = 'There are warnings in your stylesheet:\n\n';
-export default function warn(str) {
- if (warnings[warn]) {
- return;
+ if (error.fileName) {
+ message += error.fileName + '\n';
+ message += ' ' + error.sourceLinesRange;
+ message += '\n\n';
}
- warnings[warn] = true;
- console.warn('[stylemug] ' + str);
+
+ error.messages.forEach((msg, i) => {
+ message += i + 1 + ') ' + msg + '\n';
+ });
+
+ console.warn('[stylemug]', message);
}
diff --git a/scripts/dev b/scripts/dev
index 120c50a..6b36ed0 100755
--- a/scripts/dev
+++ b/scripts/dev
@@ -7,7 +7,7 @@ spawn('scripts/build', {
stdio: 'inherit',
});
-spawn('yarn', ['start'], {
+spawn('yarn', ['build'], {
stdio: 'inherit',
cwd: path.resolve('./example'),
});