diff --git a/.travis.yml b/.travis.yml
index cd2687e9..c86671fc 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,14 @@
os: osx
language: node_js
-cache:
- directories:
- - node_modules
-# - $HOME/Library/Caches/Homebrew
notifications:
email: false
+cache:
+ npm: false
node_js:
- - 'lts/*'
-# before_install:
+ - 12
+ - 14
+before_install:
+ - npm install -g npm
# - brew update
# - brew cask install sketch # install Sketch
# - mkdir -p "~/Library/Application Support/com.bohemiancoding.sketch3/Plugins" # create plugins folder
@@ -16,6 +16,7 @@ node_js:
before_script:
- npm prune
script:
+ - npm run build # smoke-test that everything transpiles correctly
- npm run test:ci
# - npm run test:e2e -- --app=/Applications/Sketch.app
# after_script:
diff --git a/README.md b/README.md
index 45f89cf4..39df04a7 100644
--- a/README.md
+++ b/README.md
@@ -35,13 +35,13 @@ Managing the assets of design systems in Sketch is complex, error-prone and time
import * as React from 'react';
import { render, Text, Artboard } from 'react-sketchapp';
-const App = props => (
+const App = (props) => (
{props.message}
);
-export default context => {
+export default (context) => {
render(, context.document.currentPage());
};
```
diff --git a/__tests__/jest/components/nodeImpl/Svg.tsx b/__tests__/jest/components/nodeImpl/Svg.tsx
index f8b71959..f69f3de4 100644
--- a/__tests__/jest/components/nodeImpl/Svg.tsx
+++ b/__tests__/jest/components/nodeImpl/Svg.tsx
@@ -9,7 +9,7 @@ jest.mock('../../../../src/jsonUtils/models', () => ({
}));
describe('node ', () => {
- it('generates the json for an svg', () => {
+ it('generates the json for an svg', async () => {
class SVGElement extends React.Component {
render() {
return (
@@ -25,6 +25,6 @@ describe('node ', () => {
}
}
- expect(ReactSketch.renderToJSON()).toMatchSnapshot();
+ expect(await ReactSketch.renderToJSON()).toMatchSnapshot();
});
});
diff --git a/__tests__/jest/sharedStyles/TextStyles.ts b/__tests__/jest/sharedStyles/TextStyles.ts
index 0d033162..fc95f6db 100644
--- a/__tests__/jest/sharedStyles/TextStyles.ts
+++ b/__tests__/jest/sharedStyles/TextStyles.ts
@@ -111,41 +111,32 @@ describe('create', () => {
});
it('only stores text attributes', () => {
- const whitelist = [
- 'color',
- 'fontFamily',
- 'fontSize',
- 'fontStyle',
- 'fontWeight',
- 'textShadowOffset',
- 'textShadowRadius',
- 'textShadowColor',
- 'textTransform',
- 'letterSpacing',
- 'lineHeight',
- 'textAlign',
- 'writingDirection',
- ];
-
- const blacklist = ['foo', 'bar', 'baz'];
-
- const input = [...whitelist, ...blacklist].reduce(
- (acc, key) => ({
- ...acc,
- [key]: '',
- }),
- {},
- );
+ const whitelist = {
+ color: 'red',
+ fontFamily: 'Helvetica',
+ fontSize: 14,
+ fontStyle: 'italic',
+ fontWeight: 'bold',
+ textShadowOffset: 2,
+ textShadowRadius: 1,
+ textShadowColor: 'black',
+ textTransform: 'uppercase',
+ letterSpacing: 1,
+ lineHeight: 18,
+ textAlign: 'left',
+ writingDirection: 'ltr',
+ };
- const res = TextStyles.create({ foo: input }, { document: doc });
+ const blacklist = { foo: 1, bar: 2, baz: 3 };
+ const res = TextStyles.create({ foo: { ...whitelist, ...blacklist } }, { document: doc });
const firstStoredStyle = res[Object.keys(res)[0]].cssStyle;
- whitelist.forEach((key) => {
- expect(firstStoredStyle).toHaveProperty(key, '');
+ Object.keys(whitelist).forEach((key) => {
+ expect(firstStoredStyle).toHaveProperty(key, whitelist[key]);
});
- blacklist.forEach((key) => {
+ Object.keys(blacklist).forEach((key) => {
expect(firstStoredStyle).not.toHaveProperty(key);
});
});
diff --git a/docs/API.md b/docs/API.md
index d1914762..779bcce6 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -29,7 +29,7 @@
- [`Symbols`](#symbols)
- [`makeSymbol`](#makesymbolnode-props-document)
-### `render(element, container)`
+### `async render(element, container?, bridge?)`
Returns the top-level rendered Sketch object or an array of Sketch objects if you use `` components.
@@ -59,6 +59,10 @@ The element to render into - will be replaced. Should either be a Sketch [Docume
Example: `sketch.getSelectedDocument().selectedPage`.
+##### `bridge` (optional)
+
+An object implementing the Platform Bridge API. When not specified, it will attempt to load the most suitable one for the platform in use among the ones bundled with the package.
+
#### Returns
The top-most rendered native Sketch layer.
@@ -69,18 +73,18 @@ The top-most rendered native Sketch layer.
import sketch from 'sketch';
import { View, Text, render } from 'react-sketchapp';
-const Document = props => (
+const Document = (props) => (
Hello world!
);
-export default () => {
- render(, sketch.getSelectedDocument().selectedPage);
+export default async () => {
+ await render(, sketch.getSelectedDocument().selectedPage);
};
```
-### `renderToJSON(element)`
+### `async renderToJSON(element, bridge?)`
Returns a Sketch JSON object for further consumption - doesn't add to the page.
@@ -90,6 +94,10 @@ Returns a Sketch JSON object for further consumption - doesn't add to the page.
Top-level React component that defines your Sketch document.
+##### `bridge` (optional)
+
+An object implementing the Platform Bridge API. When not specified, it will attempt to load the most suitable one for the platform in use among the ones bundled with the package.
+
#### Returns
The top-most Sketch layer as JSON.
@@ -684,7 +692,7 @@ Reset the registered styles.
An interface to Sketch's symbols. Create symbols and optionally inject them into the symbols page.
-### `makeSymbol(node, props, document)`
+### `makeSymbol(node, props, document, bridge?)`
Creates a new symbol and injects it into the `Symbols` page. The name of the symbol can be optionally provided and will default to the display name of the component.
@@ -699,6 +707,7 @@ Returns a react component which is an can be used to render instances of the sym
| `props.name` | `String` | The node name | Optional name for the symbol, string can include backslashes to organize these symbols with Sketch. For example `squares/blue` |
| `props.style` | [`Style`](/docs/styling.md) | | |
| `document` | `Object` | The current document | The Sketch document to make the symbol in |
+| `bridge` | `Object` | _platform-dependent_ | An object implementing the Platform Bridge API. |
### `getSymbolComponentByName(name)`
diff --git a/docs/guides/rendering.md b/docs/guides/rendering.md
index f28d47b8..097165ff 100644
--- a/docs/guides/rendering.md
+++ b/docs/guides/rendering.md
@@ -28,8 +28,7 @@ const App = () => (
export default () => {
const documents = sketch.getDocuments();
- const document =
- sketch.getSelectedDocument() || new sketch.Document(); // get the current document // or create a new document
+ const document = sketch.getSelectedDocument() || new sketch.Document(); // get the current document // or create a new document
};
```
@@ -61,8 +60,8 @@ import { render } from 'react-sketchapp';
// const App = () => ... or import App from './App';
-const getDocumentByName = name => {
- return (sketch.getDocuments() || []).find(doc => {
+const getDocumentByName = (name) => {
+ return (sketch.getDocuments() || []).find((doc) => {
return doc.path && path.basename(doc.path, '.sketch') === name;
});
};
diff --git a/docs/guides/styling.md b/docs/guides/styling.md
index a4cea7b1..1ac4e69b 100644
--- a/docs/guides/styling.md
+++ b/docs/guides/styling.md
@@ -70,12 +70,12 @@ Components use CSS styles + FlexBox layout.
## Shadow Styles
-| property | type | supported? |
-| --- | --- | --- |
-| `shadowColor` | `Color` | ✅ |
-| `shadowOffset` | `{ width: number, height: number }` | ✅ |
-| `shadowOpacity` | `number` | ✅ |
-| `shadowRadius` | `number` | `percentage` | ✅ |
+| property | type | supported? |
+| --------------- | ----------------------------------- | ---------- |
+| `shadowColor` | `Color` | ✅ |
+| `shadowOffset` | `{ width: number, height: number }` | ✅ |
+| `shadowOpacity` | `number` | ✅ |
+| `shadowRadius` | `number` | `percentage` | ✅ |
## Type Styles
@@ -102,10 +102,10 @@ Components use CSS styles + FlexBox layout.
Some properties are Sketch specific and won't work cross-platform but give you a better control over your components.
-| property | type | supported? |
-| --- | --- | --- |
-| `shadowSpread` | `number` | ✅ |
-| `shadowInner` | `boolean` | ✅ |
+| property | type | supported? |
+| -------------- | --------- | ---------- |
+| `shadowSpread` | `number` | ✅ |
+| `shadowInner` | `boolean` | ✅ |
## Examples
@@ -154,7 +154,7 @@ const colors = {
};
- {Object.keys(colors).map(name => (
+ {Object.keys(colors).map((name) => (
",
"license": "MIT",
"devDependencies": {
- "@skpm/builder": "^0.4.0",
+ "@skpm/builder": "^0.7.5",
"@types/chroma-js": "^1.3.3",
"typescript": "^3.7.2"
},
diff --git a/examples/colors/package.json b/examples/colors/package.json
index 2fe1c2d2..f5f5070d 100644
--- a/examples/colors/package.json
+++ b/examples/colors/package.json
@@ -25,6 +25,6 @@
"webpack-shell-plugin": "^0.5.0"
},
"devDependencies": {
- "@skpm/builder": "^0.4.0"
+ "@skpm/builder": "^0.7.5"
}
}
diff --git a/examples/emotion/package.json b/examples/emotion/package.json
index 576053f4..0a568c12 100644
--- a/examples/emotion/package.json
+++ b/examples/emotion/package.json
@@ -15,7 +15,7 @@
"author": "Nitin Tulswani ",
"license": "MIT",
"devDependencies": {
- "@skpm/builder": "^0.4.3"
+ "@skpm/builder": "^0.7.5"
},
"dependencies": {
"emotion-primitives": "^1.0.0-beta.6",
diff --git a/examples/form-validation/package.json b/examples/form-validation/package.json
index 3b11fc62..3c6d7473 100644
--- a/examples/form-validation/package.json
+++ b/examples/form-validation/package.json
@@ -28,6 +28,6 @@
"devDependencies": {
"extract-text-webpack-plugin": "^2.1.0",
"nwb": "^0.15.6",
- "@skpm/builder": "^0.4.0"
+ "@skpm/builder": "^0.7.5"
}
}
diff --git a/examples/foursquare-maps/package.json b/examples/foursquare-maps/package.json
index 86693d4f..2a299491 100644
--- a/examples/foursquare-maps/package.json
+++ b/examples/foursquare-maps/package.json
@@ -29,6 +29,6 @@
"react-test-renderer": "^16.3.2"
},
"devDependencies": {
- "@skpm/builder": "^0.4.0"
+ "@skpm/builder": "^0.7.5"
}
}
diff --git a/examples/glamorous/package.json b/examples/glamorous/package.json
index e12a0519..c0f60094 100644
--- a/examples/glamorous/package.json
+++ b/examples/glamorous/package.json
@@ -16,7 +16,7 @@
"author": "Nitin Tulswani ",
"license": "MIT",
"devDependencies": {
- "@skpm/builder": "^0.4.0"
+ "@skpm/builder": "^0.7.5"
},
"dependencies": {
"chroma-js": "^1.3.4",
diff --git a/examples/profile-cards-graphql/package.json b/examples/profile-cards-graphql/package.json
index a5784216..157801d6 100644
--- a/examples/profile-cards-graphql/package.json
+++ b/examples/profile-cards-graphql/package.json
@@ -27,6 +27,6 @@
"react-test-renderer": "^16.3.2"
},
"devDependencies": {
- "@skpm/builder": "^0.4.0"
+ "@skpm/builder": "^0.7.5"
}
}
diff --git a/examples/profile-cards-primitives/package.json b/examples/profile-cards-primitives/package.json
index 4f8d8b9f..19ea6f3e 100644
--- a/examples/profile-cards-primitives/package.json
+++ b/examples/profile-cards-primitives/package.json
@@ -25,6 +25,6 @@
"react-test-renderer": "^16.3.2"
},
"devDependencies": {
- "@skpm/builder": "^0.4.0"
+ "@skpm/builder": "^0.7.5"
}
}
diff --git a/examples/profile-cards-react-with-styles/package.json b/examples/profile-cards-react-with-styles/package.json
index 4797f233..be130ad5 100644
--- a/examples/profile-cards-react-with-styles/package.json
+++ b/examples/profile-cards-react-with-styles/package.json
@@ -21,6 +21,6 @@
"react-with-styles": "^1.4.0"
},
"devDependencies": {
- "@skpm/builder": "^0.4.0"
+ "@skpm/builder": "^0.7.5"
}
}
diff --git a/examples/profile-cards/package.json b/examples/profile-cards/package.json
index cb09b7de..148e5c74 100644
--- a/examples/profile-cards/package.json
+++ b/examples/profile-cards/package.json
@@ -20,6 +20,6 @@
"react-test-renderer": "^16.3.2"
},
"devDependencies": {
- "@skpm/builder": "^0.4.0"
+ "@skpm/builder": "^0.7.5"
}
}
diff --git a/examples/react-router-prototyping/src/routes/post.js b/examples/react-router-prototyping/src/routes/post.js
index e8f23c71..5851127e 100644
--- a/examples/react-router-prototyping/src/routes/post.js
+++ b/examples/react-router-prototyping/src/routes/post.js
@@ -6,7 +6,7 @@ import AppBar from '../components/AppBar';
import NavBar from '../components/NavBar';
const posts = {
- '1': {
+ 1: {
title: 'Title of a Blog Post',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
diff --git a/examples/styled-components/package.json b/examples/styled-components/package.json
index 7e6dde16..f1133286 100644
--- a/examples/styled-components/package.json
+++ b/examples/styled-components/package.json
@@ -16,7 +16,7 @@
"author": "Mathieu Dutour ",
"license": "MIT",
"devDependencies": {
- "@skpm/builder": "^0.4.0"
+ "@skpm/builder": "^0.7.5"
},
"dependencies": {
"chroma-js": "^1.2.2",
diff --git a/examples/styleguide/package.json b/examples/styleguide/package.json
index 954f2c4f..fe83f881 100644
--- a/examples/styleguide/package.json
+++ b/examples/styleguide/package.json
@@ -21,6 +21,6 @@
"react-test-renderer": "^16.3.2"
},
"devDependencies": {
- "@skpm/builder": "^0.4.0"
+ "@skpm/builder": "^0.7.5"
}
}
diff --git a/examples/symbols/package.json b/examples/symbols/package.json
index c5b0c60c..1f1a71ab 100644
--- a/examples/symbols/package.json
+++ b/examples/symbols/package.json
@@ -16,7 +16,7 @@
"author": "Jon Gold ",
"license": "MIT",
"devDependencies": {
- "@skpm/builder": "^0.4.0"
+ "@skpm/builder": "^0.7.5"
},
"dependencies": {
"react": "^16.3.2",
diff --git a/examples/timeline-airtable/package.json b/examples/timeline-airtable/package.json
index a93a6443..fec26628 100644
--- a/examples/timeline-airtable/package.json
+++ b/examples/timeline-airtable/package.json
@@ -21,6 +21,6 @@
"react-test-renderer": "^16.3.2"
},
"devDependencies": {
- "@skpm/builder": "^0.4.0"
+ "@skpm/builder": "^0.7.5"
}
}
diff --git a/package.json b/package.json
index bc038c39..3f32fafa 100644
--- a/package.json
+++ b/package.json
@@ -49,11 +49,14 @@
"invariant": "^2.2.2",
"js-sha1": "^0.6.0",
"murmur2js": "^1.0.0",
+ "node-fetch": "^2.6.0",
"node-sketch-bridge": "^0.2.0",
"normalize-css-color": "^1.0.1",
"pegjs": "^0.10.0",
"prop-types": "^15.7.2",
+ "regenerator-runtime": "^0.13.7",
"seedrandom": "^3.0.5",
+ "whatwg-url": "^7.1.0",
"yoga-layout-prebuilt": "^1.9.5"
},
"peerDependencies": {
@@ -85,10 +88,12 @@
"@types/invariant": "^2.2.31",
"@types/jest": "^25.2.1",
"@types/node": "^13.13.2",
+ "@types/node-fetch": "^2.5.4",
"@types/pegjs": "^0.10.1",
"@types/react": "^16.9.34",
"@types/react-test-renderer": "^16.9.2",
"@types/seedrandom": "^2.4.28",
+ "@types/whatwg-url": "^8.2.0",
"gitbook-cli": "^2.3.0",
"gitbook-plugin-anchorjs": "^2.1.0",
"gitbook-plugin-codeblock-disable-glossary": "0.0.1",
diff --git a/src/buildTree.ts b/src/buildTree.ts
index 246f9b9d..00e3d46c 100644
--- a/src/buildTree.ts
+++ b/src/buildTree.ts
@@ -109,6 +109,13 @@ export const buildTree = (bridge: PlatformBridge) => (element: React.ReactElemen
if (!json) {
throw new Error('Cannot render react element');
}
+
+ if (Array.isArray(json)) {
+ // TODO: It should be investigated in which cases the renderer can actually return
+ // an array and possibly handle this better instead of bailing out
+ throw new Error('Unexpected array in render result');
+ }
+
const yogaNode = computeYogaTree(bridge)(json, new Context());
yogaNode.calculateLayout(undefined, undefined, yoga.DIRECTION_LTR);
const tree = reactTreeToFlexTree(json, yogaNode, new Context());
diff --git a/src/flexToSketchJSON.ts b/src/flexToSketchJSON.ts
index 1ec0d5d9..4ba763d8 100644
--- a/src/flexToSketchJSON.ts
+++ b/src/flexToSketchJSON.ts
@@ -9,14 +9,15 @@ function missingRendererError(type: string, annotations?: string) {
);
}
-export const flexToSketchJSON = (bridge: PlatformBridge) => (
+export const flexToSketchJSON = (bridge: PlatformBridge) => async (
node: TreeNode | string,
-):
+): Promise<
| FileFormat.SymbolMaster
| FileFormat.Artboard
| FileFormat.Group
| FileFormat.ShapeGroup
- | FileFormat.SymbolInstance => {
+ | FileFormat.SymbolInstance
+> => {
if (typeof node === 'string') {
throw missingRendererError('string');
}
@@ -46,19 +47,19 @@ export const flexToSketchJSON = (bridge: PlatformBridge) => (
}
const renderer = new Renderer(bridge);
- const groupLayer = renderer.renderGroupLayer(node);
+ const groupLayer = await renderer.renderGroupLayer(node);
if (groupLayer._class === 'symbolInstance') {
return groupLayer;
}
- const backingLayers = renderer.renderBackingLayers(node);
+ const backingLayers = await renderer.renderBackingLayers(node);
// stopping the walk down the tree if we have an svg
const curriedFlexToSketchJSON = flexToSketchJSON(bridge);
const sublayers =
children && type !== 'sketch_svg'
- ? children.map((child) => curriedFlexToSketchJSON(child))
+ ? await Promise.all(children.map((child) => curriedFlexToSketchJSON(child)))
: [];
// Filter out anything null, undefined
diff --git a/src/jsonUtils/makeSvgLayer/index.ts b/src/jsonUtils/makeSvgLayer/index.ts
index 8cf96198..7c2832ea 100644
--- a/src/jsonUtils/makeSvgLayer/index.ts
+++ b/src/jsonUtils/makeSvgLayer/index.ts
@@ -78,7 +78,7 @@ function makeLayerGroup(
return group;
}
-export function makeSvgLayer(layout: LayoutInfo, name: string, svg: string) {
+export function makeSvgLayer(layout: LayoutInfo, name: string, svg: string): FileFormat.Group {
const {
data: { params, children },
} = svgModel(svg);
diff --git a/src/jsonUtils/textLayers.ts b/src/jsonUtils/textLayers.ts
index 5ea36c3e..0e83a4a9 100644
--- a/src/jsonUtils/textLayers.ts
+++ b/src/jsonUtils/textLayers.ts
@@ -127,7 +127,7 @@ const makeAttributedString = (bridge: PlatformBridge) => (
export const makeTextStyle = (bridge: PlatformBridge) => (
style: TextStyle,
- shadows?: (ViewStyle | undefined | null)[] | null,
+ shadows: (ViewStyle | undefined | null)[] | undefined | null,
): FileFormat.Style => {
const json = makeStyle(style, undefined, shadows);
json.textStyle = {
@@ -206,8 +206,8 @@ export const makeTextLayer = (bridge: PlatformBridge) => (
name: string,
textNodes: TextNode[],
_style: ViewStyle,
- resizingConstraint?: ResizeConstraints | null,
- shadows?: (ViewStyle | undefined | null)[] | null,
+ resizingConstraint: ResizeConstraints | undefined | null,
+ shadows: (ViewStyle | undefined | null)[] | undefined | null,
): FileFormat.Text => ({
_class: 'text',
do_objectID: generateID(`text:${name}-${textNodes.map((node) => node.content).join('')}`),
diff --git a/src/platformBridges/macos.ts b/src/platformBridges/macos.ts
index 6cdb85e2..68b5cd44 100644
--- a/src/platformBridges/macos.ts
+++ b/src/platformBridges/macos.ts
@@ -1,11 +1,19 @@
+// TODO: It would be better to move everything over to node-sketch-bridge
import { PlatformBridge } from '../types';
-import { createStringMeasurer, findFontName, makeImageDataFromUrl } from 'node-sketch-bridge';
+import { createStringMeasurer, findFontName } from 'node-sketch-bridge';
+import fetch from 'node-fetch';
+import { readFile as nodeReadFile } from 'fs';
const NodeMacOSBridge: PlatformBridge = {
createStringMeasurer,
findFontName,
- makeImageDataFromUrl,
+ fetch: fetch as any, // call signatures are not perfectly identical, but we'll make do
+ async readFile(path: string): Promise {
+ return new Promise((resolve, reject) => {
+ nodeReadFile(path, (err, data) => (err ? reject(err) : resolve(data)));
+ });
+ },
};
export default NodeMacOSBridge;
diff --git a/src/platformBridges/sketch/index.ts b/src/platformBridges/sketch/index.ts
index 54135f13..4824b2e3 100644
--- a/src/platformBridges/sketch/index.ts
+++ b/src/platformBridges/sketch/index.ts
@@ -1,12 +1,13 @@
import { PlatformBridge } from '../../types';
import { createStringMeasurer } from './createStringMeasurer';
import { findFontName } from './findFontName';
-import { makeImageDataFromUrl } from './makeImageDataFromUrl';
+import readFile from './readFile';
const SketchBridge: PlatformBridge = {
createStringMeasurer,
findFontName,
- makeImageDataFromUrl,
+ fetch,
+ readFile,
};
export default SketchBridge;
diff --git a/src/platformBridges/sketch/makeImageDataFromUrl.ts b/src/platformBridges/sketch/makeImageDataFromUrl.ts
deleted file mode 100644
index 440bdeef..00000000
--- a/src/platformBridges/sketch/makeImageDataFromUrl.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-export function makeImageDataFromUrl(url?: string) {
- let fetchedData = url ? NSData.dataWithContentsOfURL(NSURL.URLWithString(url)) : undefined;
-
- if (fetchedData) {
- const firstByte = String(
- NSString.alloc().initWithData_encoding(fetchedData, NSISOLatin1StringEncoding),
- ).charCodeAt(0);
-
- // Check for first byte to see if we have an image.
- // 0xFF = JPEG, 0x89 = PNG, 0x47 = GIF, 0x49 = TIFF, 0x4D = TIFF
- if (
- firstByte !== 0xff &&
- firstByte !== 0x89 &&
- firstByte !== 0x47 &&
- firstByte !== 0x49 &&
- firstByte !== 0x4d
- ) {
- fetchedData = null;
- }
- }
-
- let image: any;
-
- if (!fetchedData) {
- const errorUrl =
- 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mM8w8DwHwAEOQHNmnaaOAAAAABJRU5ErkJggg==';
- image = NSImage.alloc().initWithContentsOfURL(NSURL.URLWithString(errorUrl));
- } else {
- image = NSImage.alloc().initWithData(fetchedData);
- }
-
- let imageData: any;
-
- if (MSImageData.alloc().initWithImage_convertColorSpace !== undefined) {
- imageData = MSImageData.alloc().initWithImage_convertColorSpace(image, false);
- } else {
- imageData = MSImageData.alloc().initWithImage(image);
- }
-
- return String(
- imageData.data().base64EncodedStringWithOptions(NSDataBase64EncodingEndLineWithCarriageReturn),
- );
-}
diff --git a/src/platformBridges/sketch/readFile.ts b/src/platformBridges/sketch/readFile.ts
new file mode 100644
index 00000000..3f6bc55b
--- /dev/null
+++ b/src/platformBridges/sketch/readFile.ts
@@ -0,0 +1,5 @@
+import { readFileSync } from '@skpm/fs';
+
+export default async function readFile(path: string): Promise {
+ return Promise.resolve(readFileSync(path));
+}
diff --git a/src/render.tsx b/src/render.tsx
index 051272bb..5fa18184 100644
--- a/src/render.tsx
+++ b/src/render.tsx
@@ -40,20 +40,20 @@ const getDefaultPage = (): SketchLayer => {
return isNativeSymbolsPage(currentPage) ? doc.addBlankPage() : currentPage;
};
-const renderContents = (bridge: PlatformBridge) => (
+const renderContents = (bridge: PlatformBridge) => async (
tree: TreeNode | string,
container: SketchLayer,
-): SketchLayer => {
- const json = flexToSketchJSON(bridge)(tree);
+): Promise => {
+ const json = await flexToSketchJSON(bridge)(tree);
const layer = fromSJSON(json, '119');
return renderLayers([layer], container);
};
-const renderPage = (bridge: PlatformBridge) => (
+const renderPage = (bridge: PlatformBridge) => async (
tree: TreeNode,
page: SketchPage,
-): Array => {
+): Promise> => {
const children = tree.children || [];
// assume if name is set on this nested page, the intent is to overwrite
diff --git a/src/renderToJSON.ts b/src/renderToJSON.ts
index 133690d1..56ae454e 100644
--- a/src/renderToJSON.ts
+++ b/src/renderToJSON.ts
@@ -4,9 +4,9 @@ import { buildTree } from './buildTree';
import { flexToSketchJSON } from './flexToSketchJSON';
import * as React from 'react';
-export const renderToJSON = (platformBridge: PlatformBridge) => (
+export const renderToJSON = (platformBridge: PlatformBridge) => async (
element: React.ReactElement,
-): FileFormat.AnyLayer => {
+): Promise => {
const tree = buildTree(platformBridge)(element);
return flexToSketchJSON(platformBridge)(tree);
};
diff --git a/src/renderers/ArtboardRenderer.ts b/src/renderers/ArtboardRenderer.ts
index 93950c76..8bbcf259 100644
--- a/src/renderers/ArtboardRenderer.ts
+++ b/src/renderers/ArtboardRenderer.ts
@@ -6,7 +6,7 @@ import { TreeNode } from '../types';
import { Props } from '../components/Artboard';
export class ArtboardRenderer extends SketchRenderer {
- renderGroupLayer({ layout, style, props }: TreeNode): FileFormat.Artboard {
+ async renderGroupLayer({ layout, style, props }: TreeNode): Promise {
let color: FileFormat.Color | undefined;
if (style.backgroundColor !== undefined) {
color = makeColorFromCSS(style.backgroundColor);
diff --git a/src/renderers/ImageRenderer.ts b/src/renderers/ImageRenderer.ts
index e444a87a..d45779ee 100644
--- a/src/renderers/ImageRenderer.ts
+++ b/src/renderers/ImageRenderer.ts
@@ -16,7 +16,7 @@ function extractURLFromSource(source?: string | { uri?: string } | null): string
}
export class ImageRenderer extends SketchRenderer {
- renderBackingLayers({
+ async renderBackingLayers({
layout,
style,
props,
@@ -32,7 +32,7 @@ export class ImageRenderer extends SketchRenderer {
const url = extractURLFromSource(props.source);
- const image = getImageDataFromURL(this.platformBridge)(url);
+ const image = await getImageDataFromURL(this.platformBridge)(url);
const fillImage = makeJSONDataReference(image);
diff --git a/src/renderers/SketchRenderer.ts b/src/renderers/SketchRenderer.ts
index 3fca03f2..85208e03 100644
--- a/src/renderers/SketchRenderer.ts
+++ b/src/renderers/SketchRenderer.ts
@@ -17,16 +17,17 @@ export class SketchRenderer {
return 'Group';
}
- renderGroupLayer({
+ async renderGroupLayer({
layout,
style,
props,
- }: TreeNode):
+ }: TreeNode): Promise<
| FileFormat.SymbolMaster
| FileFormat.Artboard
| FileFormat.Group
| FileFormat.ShapeGroup
- | FileFormat.SymbolInstance {
+ | FileFormat.SymbolInstance
+ > {
// Default SketchRenderer just renders an empty group
const transform = processTransform(layout, style);
@@ -49,23 +50,25 @@ export class SketchRenderer {
};
}
- renderBackingLayers(
+ async renderBackingLayers(
_node: TreeNode,
- ): (
- | FileFormat.ShapePath
- | FileFormat.Rectangle
- | FileFormat.SymbolMaster
- | FileFormat.Group
- | FileFormat.Polygon
- | FileFormat.Star
- | FileFormat.Triangle
- | FileFormat.ShapeGroup
- | FileFormat.Text
- | FileFormat.SymbolInstance
- | FileFormat.Slice
- | FileFormat.Hotspot
- | FileFormat.Bitmap
- )[] {
+ ): Promise<
+ (
+ | FileFormat.ShapePath
+ | FileFormat.Rectangle
+ | FileFormat.SymbolMaster
+ | FileFormat.Group
+ | FileFormat.Polygon
+ | FileFormat.Star
+ | FileFormat.Triangle
+ | FileFormat.ShapeGroup
+ | FileFormat.Text
+ | FileFormat.SymbolInstance
+ | FileFormat.Slice
+ | FileFormat.Hotspot
+ | FileFormat.Bitmap
+ )[]
+ > {
return [];
}
}
diff --git a/src/renderers/SvgRenderer.ts b/src/renderers/SvgRenderer.ts
index 0b484311..7ef1929a 100644
--- a/src/renderers/SvgRenderer.ts
+++ b/src/renderers/SvgRenderer.ts
@@ -66,8 +66,8 @@ export class SvgRenderer extends ViewRenderer {
return props.name || 'Svg';
}
- renderBackingLayers(node: TreeNode) {
- const layers = super.renderBackingLayers(node);
+ async renderBackingLayers(node: TreeNode) {
+ const layers = await super.renderBackingLayers(node);
const { layout, props, children, style } = node;
diff --git a/src/renderers/SymbolInstanceRenderer.ts b/src/renderers/SymbolInstanceRenderer.ts
index 36e44b04..882aa30b 100644
--- a/src/renderers/SymbolInstanceRenderer.ts
+++ b/src/renderers/SymbolInstanceRenderer.ts
@@ -98,11 +98,10 @@ const extractOverrides = (layers: FileFormat.AnyLayer[] = [], path?: string): Ov
};
export class SymbolInstanceRenderer extends SketchRenderer {
- renderGroupLayer({
+ async renderGroupLayer({
layout,
props,
}: TreeNode) {
- const bridge = this.platformBridge;
const masterTree = getSymbolMasterById(props.symbolID);
if (!masterTree) {
@@ -126,8 +125,9 @@ export class SymbolInstanceRenderer extends SketchRenderer {
const overridableLayers = extractOverrides(masterTree.layers);
- const overrideValues = overridableLayers.reduce(function inject(
- memo: FileFormat.OverrideValue[],
+ const overrideValues = await overridableLayers.reduce(async function inject(
+ this: SymbolInstanceRenderer,
+ memo: Promise,
reference: Override,
) {
if (reference.type === 'symbolID') {
@@ -163,7 +163,9 @@ export class SymbolInstanceRenderer extends SketchRenderer {
);
}
- memo.push(makeOverride(reference.path, reference.type, replacementMaster.symbolID));
+ (await memo).push(
+ makeOverride(reference.path, reference.type, replacementMaster.symbolID),
+ );
extractOverrides(replacementMaster.layers, newPath).reduce(inject, memo);
@@ -187,7 +189,7 @@ export class SymbolInstanceRenderer extends SketchRenderer {
`The override value of a Text must be a string.\n\nIn Symbol Instance: "${props.name}"\nFor Override: "${reference.name}"`,
);
}
- memo.push(makeOverride(reference.path, reference.type, overrideValue));
+ (await memo).push(makeOverride(reference.path, reference.type, overrideValue));
}
if (reference.type === 'image') {
@@ -196,18 +198,18 @@ export class SymbolInstanceRenderer extends SketchRenderer {
`The override value of an Image must be a url.\n\nIn Symbol Instance: "${props.name}"\nFor Override: "${reference.name}"`,
);
}
- memo.push(
+ (await memo).push(
makeOverride(
reference.path,
reference.type,
- makeJSONDataReference(getImageDataFromURL(bridge)(overrideValue)),
+ makeJSONDataReference(await getImageDataFromURL(this.platformBridge)(overrideValue)),
),
);
}
return memo;
},
- []);
+ Promise.resolve([]));
symbolInstance.overrideValues = overrideValues;
diff --git a/src/renderers/SymbolMasterRenderer.ts b/src/renderers/SymbolMasterRenderer.ts
index 6bed91a0..29c0c3a0 100644
--- a/src/renderers/SymbolMasterRenderer.ts
+++ b/src/renderers/SymbolMasterRenderer.ts
@@ -4,7 +4,7 @@ import { TreeNode } from '../types';
import { SymbolMasterProps } from '../symbol';
export class SymbolMasterRenderer extends SketchRenderer {
- renderGroupLayer({
+ async renderGroupLayer({
layout,
props,
}: TreeNode) {
diff --git a/src/renderers/TextRenderer.ts b/src/renderers/TextRenderer.ts
index 9176153d..954e702a 100644
--- a/src/renderers/TextRenderer.ts
+++ b/src/renderers/TextRenderer.ts
@@ -10,7 +10,7 @@ export class TextRenderer extends SketchRenderer {
return props.name || 'Text';
}
- renderBackingLayers({ layout, style, textStyle, props }: TreeNode) {
+ async renderBackingLayers({ layout, style, textStyle, props }: TreeNode) {
// Append all text nodes's content into one string if name is missing
const resolvedName = props.name
? props.name
diff --git a/src/renderers/ViewRenderer.ts b/src/renderers/ViewRenderer.ts
index ccdc43b2..abc2718f 100644
--- a/src/renderers/ViewRenderer.ts
+++ b/src/renderers/ViewRenderer.ts
@@ -38,25 +38,27 @@ export class ViewRenderer extends SketchRenderer {
return 'View';
}
- renderBackingLayers({
+ async renderBackingLayers({
layout,
style,
props,
- }: TreeNode): (
- | FileFormat.ShapePath
- | FileFormat.Rectangle
- | FileFormat.SymbolMaster
- | FileFormat.Group
- | FileFormat.Polygon
- | FileFormat.Star
- | FileFormat.Triangle
- | FileFormat.ShapeGroup
- | FileFormat.Text
- | FileFormat.SymbolInstance
- | FileFormat.Slice
- | FileFormat.Hotspot
- | FileFormat.Bitmap
- )[] {
+ }: TreeNode): Promise<
+ (
+ | FileFormat.ShapePath
+ | FileFormat.Rectangle
+ | FileFormat.SymbolMaster
+ | FileFormat.Group
+ | FileFormat.Polygon
+ | FileFormat.Star
+ | FileFormat.Triangle
+ | FileFormat.ShapeGroup
+ | FileFormat.Text
+ | FileFormat.SymbolInstance
+ | FileFormat.Slice
+ | FileFormat.Hotspot
+ | FileFormat.Bitmap
+ )[]
+ > {
let layers: FileFormat.ShapeGroup[] = [];
// NOTE(lmr): the group handles the position, so we just care about width/height here
const {
diff --git a/src/sharedStyles/TextStyles.ts b/src/sharedStyles/TextStyles.ts
index df4b4e5a..93b1966c 100644
--- a/src/sharedStyles/TextStyles.ts
+++ b/src/sharedStyles/TextStyles.ts
@@ -7,6 +7,7 @@ import {
PlatformBridge,
} from '../types';
import { getSketchVersion } from '../utils/getSketchVersion';
+import { isRunningInSketch } from '../utils/isRunningInSketch';
import { hashStyle } from '../utils/hashStyle';
import { getDocument } from '../utils/getDocument';
import { sharedTextStyles } from '../utils/sharedTextStyles';
@@ -41,7 +42,7 @@ export const TextStyles = (getDefaultBridge: () => PlatformBridge) => ({
) {
const safeStyle = pick(style, INHERITABLE_FONT_STYLES);
const hash = hashStyle(safeStyle);
- const sketchStyle = makeTextStyle(platformBridge)(safeStyle);
+ const sketchStyle = makeTextStyle(platformBridge)(safeStyle, undefined);
const sharedObjectID = sharedTextStyles.addStyle(name, sketchStyle);
// FIXME(gold): side effect :'(
@@ -66,7 +67,7 @@ export const TextStyles = (getDefaultBridge: () => PlatformBridge) => ({
const sketchVersion = getSketchVersion();
- if (sketchVersion !== 'NodeJS' && sketchVersion < 50) {
+ if (isRunningInSketch() && sketchVersion < 50) {
if (doc) {
doc.showMessage('💎 Requires Sketch 50+ 💎');
}
diff --git a/src/symbol.tsx b/src/symbol.tsx
index 2b958f5b..db7d2fa5 100644
--- a/src/symbol.tsx
+++ b/src/symbol.tsx
@@ -13,7 +13,7 @@ import { renderLayers } from './render';
import { resetLayer } from './resets';
import { getDocumentData } from './utils/getDocument';
import { SketchDocumentData, SketchDocument, WrappedSketchDocument, PlatformBridge } from './types';
-import { getSketchVersion } from './utils/getSketchVersion';
+import { isRunningInSketch } from './utils/isRunningInSketch';
let id = 0;
const nextId = () => ++id;
@@ -70,8 +70,8 @@ const getExistingSymbols = (documentData: SketchDocumentData) => {
export const injectSymbols = (
document?: SketchDocumentData | SketchDocument | WrappedSketchDocument,
) => {
- if (getSketchVersion() === 'NodeJS') {
- console.error('Cannot inject symbols in NodeJS');
+ if (!isRunningInSketch()) {
+ console.error('Cannot inject symbols while Sketch is not running');
return;
}
@@ -153,12 +153,12 @@ const SymbolMasterPropTypes = {
export type SymbolMasterProps = PropTypes.InferProps;
-export const makeSymbol = (bridge: PlatformBridge) => (
+export const makeSymbol = (bridge: PlatformBridge) => async (
Component: React.ComponentType,
symbolProps: string | SymbolMasterProps,
- document?: SketchDocumentData | SketchDocument | WrappedSketchDocument,
+ document: SketchDocumentData | SketchDocument | WrappedSketchDocument | undefined,
) => {
- if (!hasInitialized && getSketchVersion() !== 'NodeJS') {
+ if (!hasInitialized && isRunningInSketch()) {
const documentData = getDocumentData(document);
if (documentData) {
getExistingSymbols(documentData);
@@ -173,7 +173,7 @@ export const makeSymbol = (bridge: PlatformBridge) => (
? existingSymbol.symbolID
: generateID(`symbolID:${masterName}`, !!masterName);
- const symbolMaster = flexToSketchJSON(bridge)(
+ const symbolMaster = (await flexToSketchJSON(bridge)(
buildTree(bridge)(
(
,
),
- ) as FileFormat.SymbolMaster;
+ )) as FileFormat.SymbolMaster;
symbolsRegistry[symbolID] = symbolMaster;
return createSymbolInstanceClass(symbolMaster);
@@ -236,7 +236,7 @@ export const getSymbolMasterByName = (
? Object.keys(symbolsRegistry).find((key) => String(symbolsRegistry[key].name) === name)
: '';
- if (typeof symbolID === 'undefined' && name && getSketchVersion() !== 'NodeJS') {
+ if (typeof symbolID === 'undefined' && name && isRunningInSketch()) {
return tryGettingSymbolMasterInDocumentByName(name, document);
}
@@ -253,7 +253,7 @@ export const getSymbolMasterById = (
): FileFormat.SymbolMaster | undefined => {
let symbolMaster = symbolID ? symbolsRegistry[symbolID] : undefined;
- if (typeof symbolMaster === 'undefined' && symbolID && getSketchVersion() !== 'NodeJS') {
+ if (typeof symbolMaster === 'undefined' && symbolID && isRunningInSketch()) {
symbolMaster = tryGettingSymbolMasterInDocumentById(symbolID, document);
}
diff --git a/src/types/index.ts b/src/types/index.ts
index 2953c397..97ad6d6f 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -113,16 +113,17 @@ export type TreeNode = {
};
export type ResizeConstraints = {
- top?: boolean;
- right?: boolean;
- bottom?: boolean;
- left?: boolean;
- fixedHeight?: boolean;
- fixedWidth?: boolean;
+ top?: boolean | null;
+ right?: boolean | null;
+ bottom?: boolean | null;
+ left?: boolean | null;
+ fixedHeight?: boolean | null;
+ fixedWidth?: boolean | null;
};
export type PlatformBridge = {
createStringMeasurer(textNodes: TextNode[], maxWidth: number): Size;
findFontName(style: TextStyle): string;
- makeImageDataFromUrl(url?: string): string;
+ fetch(input: RequestInfo, init?: RequestInit): Promise;
+ readFile(path: string): Promise;
};
diff --git a/src/types/skpm__fs.d.ts b/src/types/skpm__fs.d.ts
new file mode 100644
index 00000000..45b4e8d9
--- /dev/null
+++ b/src/types/skpm__fs.d.ts
@@ -0,0 +1 @@
+declare module '@skpm/fs';
diff --git a/src/utils/getImageDataFromURL.ts b/src/utils/getImageDataFromURL.ts
index 2e5a4a4f..38b416cc 100644
--- a/src/utils/getImageDataFromURL.ts
+++ b/src/utils/getImageDataFromURL.ts
@@ -1,13 +1,63 @@
import sha1 from 'js-sha1';
import { PlatformBridge } from '../types';
+import { URL } from 'whatwg-url';
-export const getImageDataFromURL = (bridge: PlatformBridge) => (
+const ERROR_IMAGE =
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mM8w8DwHwAEOQHNmnaaOAAAAABJRU5ErkJggg==';
+
+const ERROR_RESULT = {
+ data: ERROR_IMAGE,
+ sha1: sha1(ERROR_IMAGE),
+};
+
+const isImage = (buffer: Buffer): boolean => {
+ const firstByte = buffer[0];
+
+ // Check for first byte to see if we have an image.
+ // 0xFF = JPEG, 0x89 = PNG, 0x47 = GIF, 0x49 = TIFF, 0x4D = TIFF
+ return (
+ firstByte === 0xff ||
+ firstByte === 0x89 ||
+ firstByte === 0x47 ||
+ firstByte === 0x49 ||
+ firstByte === 0x4d
+ );
+};
+
+const fetchRemoteImage = async (url: string, { fetch }: PlatformBridge): Promise => {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`${response.status}`);
+ }
+
+ return Buffer.from(await response.arrayBuffer());
+};
+
+export const getImageDataFromURL = (bridge: PlatformBridge) => async (
url?: string,
-): { data: string; sha1: string } => {
- const data = bridge.makeImageDataFromUrl(url);
+): Promise> => {
+ if (!url) {
+ return ERROR_RESULT;
+ }
+
+ try {
+ const parsedUrl = new URL(url);
+
+ const buffer = await (parsedUrl.protocol === 'file:'
+ ? bridge.readFile(parsedUrl.pathname)
+ : fetchRemoteImage(url, bridge));
+
+ if (!isImage(buffer)) throw new Error('Unrecognized image format');
+
+ const data = buffer.toString('base64');
+
+ return {
+ data,
+ sha1: sha1(data),
+ };
+ } catch (error) {
+ console.error(`Error while fetching '${url}':`, error);
- return {
- data,
- sha1: sha1(data),
- };
+ return ERROR_RESULT;
+ }
};
diff --git a/src/utils/isRunningInSketch.ts b/src/utils/isRunningInSketch.ts
new file mode 100644
index 00000000..a9cdf679
--- /dev/null
+++ b/src/utils/isRunningInSketch.ts
@@ -0,0 +1,5 @@
+import { getSketchVersion } from './getSketchVersion';
+
+export function isRunningInSketch() {
+ return getSketchVersion() !== 'NodeJS';
+}
diff --git a/tsconfig.json b/tsconfig.json
index 7a12bc88..99dd0b5e 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -8,9 +8,9 @@
"declaration": true,
"inlineSourceMap": false,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
-
"strict": true /* Enable all strict type-checking options. */,
-
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
"lib": ["es5", "dom"],
"types": ["jest", "node"],
"typeRoots": ["node_modules/@types", "src/types"],