How to use module import in nodejs

Issue #752

Use esm

1
npm install esm

In our code, import as normal

1
2
3
4
const fs = require('fs');
// intended to be run after babel, and in ./dist folder
import factory from 'data'
const shell = require('shelljs')

Then use esm to convert

1
2
3
4
5
6
7
8
{
"name": "generator",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "npx babel index.js --out-file dist/index.js && node -r esm dist/index.js"
}
}

Read more

How to use relative file module with Create React app

Issue #751

Declare data/package.json to make it into node module

1
2
3
4
5
6
7
{
"name": "data",
"version": "0.1.0",
"private": true,
"homepage": ".",
"main": "main.js"
}

Then in landing/package.json, use file

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name": "landing",
"version": "0.1.0",
"private": true,
"homepage": ".",
"dependencies": {
"@emotion/core": "^10.0.28",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-image": "^2.4.0",
"react-scripts": "3.4.1",
"data": "file:../data"
},

How to copy folder in nodej

Issue #651

Use shelljs

1
npm install shelljs
1
2
const shell = require('shelljs')
shell.exec(`cp -a source_path/. destination_path`)

The -a option is an improved recursive option, that preserve all file attributes, and also preserve symlinks.

The . at end of the source path is a specific cp syntax that allow to copy all files and folders, included hidden ones.
Updated at 2020-05-07 04:10:05

How to use babel 7 in node project

Issue #650

Install

1
2
3
npm install @babel/core
npm install @babel/cli
npm install @babel/preset-env

Configure .babelrc

1
2
3
{
"presets": ["@babel/preset-env"]
}

In package.json, transpile using npx babel then node dist/index.js

1
"start": "cp ../landing/src/apps/factory.js copied/factory.js && npx babel index.js --out-file dist/index.js && npx babel copied/factory.js --out-file dist/factory.js && node dist/index.js"

How to apply translations to Localizable.strings

Issue #492

Suppose we have a base Localizable.strings

1
2
"open" = "Open";
"closed" = "Closed";

After sending that file for translations, we get translated versions.

1
2
"open" = "Åpen";
"closed" = "Stengt";

Searching and copy pasting these to our Localizable.strings is tedious and time consuming. We can write a script to apply that.

Remember that we need to be aware of smart and dump quotes

1
2
.replace(/\"/g, '')
.replace(/\"/g, '')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const fs = require('fs')

const originalFile = 'MyApp/Resources/nb.lproj/Localizable.strings'
const translationString = `
"open" = "Åpen";
"closed" = "Stengt";
`

class Translation {
constructor(key, value) {
this.key = key
this.value = value
}
}

Translation.make = function make(line) {
if (!line.endsWith(';')) {
return new Translation('', '')
}

const parts = line
.replace(';')
.split(" = ")

const key = parts[0]
.replace(/\"/g, '')
.replace(/\”/g, '')
const value = parts[1]
.replace(/\"/g, '')
.replace(/\”/g, '')
.replace('undefined', '')
return new Translation(key, value)
}

function main() {
const translations = translationString
.split(/\r?\n/)
.map((line) => { return Translation.make(line) })

apply(translations, originalFile)
}

function apply(translations, originalFile) {
try {
const originalData = fs.readFileSync(originalFile, 'utf8')
const originalLines = originalData.split(/\r?\n/)
const parsedLine = originalLines.map((originalLine) => {
const originalTranslation = Translation.make(originalLine)
const find = translations.find((translation) => { return translation.key === originalTranslation.key })
if (originalLine !== "" && find !== undefined && find.key !== "") {
return `"${find.key}" = "${find.value}";`
} else {
return originalLine
}
})

const parsedData = parsedLine.join('\n')
fs.writeFileSync(originalFile, parsedData, { overwrite: true })
} catch (err) {
console.error(err)
}
}

main()

How to notarize electron app

Issue #430

Use electron builder

1
npm install electron-builder@latest --save-dev

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"name": "icon_generator",
"version": "1.3.0",
"description": "A macOS app to generate app icons",
"main": "babel/main.js",
"repository": "https://github.com/onmyway133/IconGenerator",
"author": "Khoa Pham",
"license": "MIT",
"scripts": {
"start": "npm run babel && electron .",
"babel": "babel ./src --out-dir ./babel --copy-files",
"dist": "npm run babel && electron-builder"
},
"build": {
"appId": "com.onmyway133.IconGenerator",
"buildVersion": "20",
"productName": "Icon Generator",
"icon": "./Icon/Icon.icns",
"mac": {
"category": "public.app-category.productivity",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "./entitlements.plist",
"entitlementsInherit": "./entitlements.plist"
},
"win": {
"target": "msi"
},
"linux": {
"target": [
"AppImage",
"deb"
]
},
"afterSign": "./afterSignHook.js"
}
}

Declare entitlements

entitlements.plist

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>

Use electron-notarize

afterSignHook.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const fs = require('fs');
const path = require('path');
var electron_notarize = require('electron-notarize');

module.exports = async function (params) {
// Only notarize the app on Mac OS only.
if (process.platform !== 'darwin') {
return;
}
console.log('afterSign hook triggered', params);

// Same appId in electron-builder.
let appId = 'com.onmyway133.IconGenerator'

let appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`);
if (!fs.existsSync(appPath)) {
throw new Error(`Cannot find application at: ${appPath}`);
}

console.log(`Notarizing ${appId} found at ${appPath}`);

try {
await electron_notarize.notarize({
appBundleId: appId,
appPath: appPath,
appleId: process.env.appleId,
appleIdPassword: process.env.appleIdPassword,
});
} catch (error) {
console.error(error);
}

console.log(`Done notarizing ${appId}`);
};

Run

Generate password for Apple Id because of 2FA

1
2
3
export appleId=onmyway133@gmail.com
export appleIdPassword=1234-abcd-efgh-7890
npm run dist

Check

1
spctl --assess --verbose Icon\ Generator.app

Troubleshooting

babel

  • Since electron-builder create dist folder for distribution, for example dist/mac/Icon Generator, I’ve renamed babel generated code to babel directory

babel 6 regeneratorRuntime is not defined

It is because of afterSignHook. Ignore in .babelrc not work

1
2
3
4
5
6
7
8
9
{
"plugins": [
"transform-react-jsx-source"
],
"presets": ["env", "react"],
"ignore": [
"afterSignHook.js"
]
}

Should use babel 7 with babel.config.js

1
2
npm install --save @babel/runtime 
npm install --save-dev @babel/plugin-transform-runtime

Use electron-forge

https://httptoolkit.tech/blog/notarizing-electron-apps-with-electron-forge/

Read more

How to read and write file using fs in node

Issue #419

1
2
3
4
5
6
7
8
9
10
11
12
function write(json) {
const data = JSON.stringify(json)
const year = json.date.getFullYear()
const directory = `collected/${slugify(className)}/${year}`

fs.mkdirSync(directory, { recursive: true })
fs.writeFileSync(
`${directory}/${slugify(studentName)}`,
data,
{ overwrite: true }
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
async function readAll() {
const classes = fs.readdirSync('classes')
classes.forEach((class) => {
const years = fs.readdirSync(`classes/${class}`)
years.forEach((year) => {
const students = fs.readdirSync(`classes/${class}/${year}`)
students.forEach((student) => {
const data = fs.readFileSync(`classes/${class}/${year}/${student})
const json = JSON.parse(data)
})
})
})
}

How to get videos from vimeo in node

Issue #418

Code

Path for user users/nsspain/videos
Path for showcase https://developer.vimeo.com/api/reference/albums#get_album
Path for Channels, Groups and Portfolios

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const Vimeo = require('vimeo').Vimeo
const vimeoClient = new Vimeo(vimeoClientId, vimeoClientSecret, vimeoAccessToken)

async getVideos(path) {
const options = {
path: `channels/staffpicks/videos`,
query: {
page: 1,
per_page: 100,
fields: 'uri,name,description,created_time,pictures'
}
}

return new Promise((resolve, reject) => {
try {
vimeoClient.request(options, (error, body, status_code, headers) => {
if (isValid(body)) {
resolve(body)
} else {
throw error
}
})
} catch (e) {
reject(e)
console.log(e)
}
})
}

Response look like

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
{
"total":13754,
"page":1,
"per_page":100,
"paging":{
"next":"/channels/staffpicks/videos?page=2&per_page=100&fields=uri%2Cname%2Cdescription%2Ccreated_time%2Cpictures",
"previous":null,
"first":"/channels/staffpicks/videos?page=1&per_page=100&fields=uri%2Cname%2Cdescription%2Ccreated_time%2Cpictures",
"last":"/channels/staffpicks/videos?page=138&per_page=100&fields=uri%2Cname%2Cdescription%2Ccreated_time%2Cpictures"
},
"data":[
{
"uri":"/videos/359281775",
"name":"Maestro",
"description":"A Bloom Pictures short film directed by Illogic.\n\n\"Maestro\" is this week's Staff Pick Premiere. Read more about it on the Vimeo Blog: https://vimeo.com/blog/post/staff-pick-premiere-maestro-from-illogic\n\nMaking of :\n https://vimeo.com/bloompictures/maestromakingof\n\nYou want to collaborate?\nSend us a message at : hello@bloompictures.tv\n\nFor festivals and screenings, please contact : \nfestival@miyu.fr\n\nPress/Media requests : \nbenoit@animationshowcase.com\n\nhttps://www.bloompictures.tv\n\n©Bloom Pictures 2019",
"created_time":"2019-09-11T12:31:33+00:00",
"pictures":{
"uri":"/videos/359281775/pictures/813130850",
"active":true,
"type":"custom",
"sizes":[
{
"width":100,
"height":75,
"link":"https://i.vimeocdn.com/video/813130850_100x75.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_100x75.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":200,
"height":150,
"link":"https://i.vimeocdn.com/video/813130850_200x150.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_200x150.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":295,
"height":166,
"link":"https://i.vimeocdn.com/video/813130850_295x166.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_295x166.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":640,
"height":360,
"link":"https://i.vimeocdn.com/video/813130850_640x360.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_640x360.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":1280,
"height":720,
"link":"https://i.vimeocdn.com/video/813130850_1280x720.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_1280x720.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":1920,
"height":1080,
"link":"https://i.vimeocdn.com/video/813130850_1920x1080.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_1920x1080.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":640,
"height":346,
"link":"https://i.vimeocdn.com/video/813130850_640x346.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_640x346.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":960,
"height":519,
"link":"https://i.vimeocdn.com/video/813130850_960x519.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_960x519.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":1280,
"height":692,
"link":"https://i.vimeocdn.com/video/813130850_1280x692.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_1280x692.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":1920,
"height":1038,
"link":"https://i.vimeocdn.com/video/813130850_1920x1038.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_1920x1038.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
},
{
"width":1280,
"height":692,
"link":"https://i.vimeocdn.com/video/813130850_1280x692.jpg?r=pad",
"link_with_play_button":"https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F813130850_1280x692.jpg&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png"
}
],
"resource_key":"b42ac645a67b3277cb2fe66d3894016842ceef72"
}
}
]
}

Read more

How to get videos from youtube in node

Issue #417

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Youtube {
async getVideos(playlistId, pageToken) {
const options = {
key: clientKey,
part: 'id,contentDetails,snippet',
playlistId: playlistId,
maxResult: 100,
pageToken
}

return new Promise((resolve, reject) => {
try {
youtube.playlistItems.list(options, (error, result) => {
if (isValid(result)) {
resolve(result)
} else {
throw error
}
})
} catch (e) {
reject(e)
}
})
}
}

Response look like

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
{
"kind": "youtube#playlistItemListResponse",
"etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/ZNTrH71d3sV6gR6BWPeamXI1HhE\"",
"nextPageToken": "CAUQAA",
"pageInfo": {
"totalResults": 32,
"resultsPerPage": 5
},
"items": [
{
"kind": "youtube#playlistItem",
"etag": "\"p4VTdlkQv3HQeTEaXgvLePAydmU/pt-bElhU3f7Q6c1Wc0URk9GJN-w\"",
"id": "UExDbDVOTTRxRDN1X0w4ZEpyV1liTEI4RmNVYW9BSERGdC4yODlGNEE0NkRGMEEzMEQy",
"snippet": {
"publishedAt": "2019-04-11T06:09:26.000Z",
"channelId": "UCuPue-GLK4nVX8klxQITIOw",
"title": "abc",
"description": "abc",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/ZefmzgLabCA/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/ZefmzgLabCA/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/ZefmzgLabCA/hqdefault.jpg",
"width": 480,
"height": 360
},
"standard": {
"url": "https://i.ytimg.com/vi/ZefmzgLabCA/sddefault.jpg",
"width": 640,
"height": 480
},
"maxres": {
"url": "https://i.ytimg.com/vi/ZefmzgLabCA/maxresdefault.jpg",
"width": 1280,
"height": 720
}
},
"channelTitle": "try! Swift Conference",
"playlistId": "PLCl5NM4qD3u_L8dJrWYbLB8FcUaoAHDFt",
"position": 0,
"resourceId": {
"kind": "youtube#video",
"videoId": "ZefmzgLabCA"
}
}
}
]
}

To handle pagination

1
2
3
4
5
6
7
8
9
async getVideosLoop(playlistId, nextPageToken, items, count) {
const response = await this.getVideos(playlistId, nextPageToken)
const newItems = items.concat(response.data.items)
if (isValid(response.data.nextPageToken) && count < 10) {
return this.getVideosLoop(playlistId, response.data.nextPageToken, newItems, count + 1)
} else {
return newItems
}
}

To get playlist title, use playlists.list

Read more

How to make collaborative drawing canvas with socketio and node

Issue #399

Client

App.js

1
2
3
4
5
6
7
8
9
10
11
12
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import Main from './Main'

class App extends Component {
render() {
return <Main />
}
}

export default App;

Main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// @flow

import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';

import Manager from './Manager'

const styles = {
root: {
flexGrow: 1,
},
grow: {
flexGrow: 1,
},
menuButton: {
marginLeft: -12,
marginRight: 20,
},
};

class Main extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
}

render() {
const { classes } = this.props;

return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<IconButton className={classes.menuButton} color="inherit" aria-label="Menu">
<MenuIcon />
</IconButton>
<Typography variant="h6" color="inherit" className={classes.grow}>
Collaborate Canvas
</Typography>
<Button color="inherit" onClick={this.onImagePress} >Image</Button>
<Button color="inherit" onClick={this.onClearPress} >Clear</Button>
<input ref="fileInput" type="file" id="myFile" multiple accept="image/*" style={{display: 'none'}} onChange={this.handleFiles}></input>
</Toolbar>
</AppBar>
<canvas ref="canvas" with="1000" height="1000"></canvas>
</div>
)
}

componentDidMount() {
const canvas = this.refs.canvas
this.manager = new Manager(canvas)
this.manager.connect()
}

onImagePress = () => {
const fileInput = this.refs.fileInput
fileInput.click()
}

onClearPress = () => {
this.manager.clear()
}

handleFiles = (e) => {
e.persist()
const canvas = this.refs.canvas
const context = canvas.getContext('2d')

const file = e.target.files[0]
var image = new Image()
image.onload = function() {
context.drawImage(image, 0, 0, window.innerWidth, window.innerHeight)
}
image.src = URL.createObjectURL(file)
}
}

export default withStyles(styles)(Main);

Server

Use express and socket.io

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// @flow

const express = require('express')
const app = express()
const http = require('http')
const socketIO = require('socket.io')

const server = http.createServer(app)
const io = socketIO.listen(server)
server.listen(3001)
app.use(express.static(__dirname + '/public'))
console.log("Server running on 127.0.0.1:8080")

let lines = []
io.on('connection', (socket) => {
lines.forEach((line) => {
const data = { line }
socket.emit('draw_line', data)
})

socket.on('draw_line', (data) => {
const { line } = data
lines.push(line)

io.emit('draw_line', data)
})

socket.on('clear', () => {
lines = []
io.emit('clear')
})

socket.on('draw_image', (data) => {
io.emit('draw_image', data)
})
})

How to generate changelog for GitHub releases with rxjs and node

Issue #398

How to

Technical

Dependencies

1
2
3
4
const Rx = require('rxjs/Rx')
const Fetch = require('node-fetch')
const Minimist = require('minimist')
const Fs = require('fs')

Use GraphQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
makeOptions(query, token) {
return {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `bearer ${token}`
},
body: JSON.stringify({
query: `
query {
repository(owner: "${this.owner}", name: "${this.repo}") {
${query}
}
}
`
})
}
}

Use orderBy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fetchPRsAndIssues(dates) {
const query = `
pullRequests(last: 100, orderBy: {field: UPDATED_AT, direction: ASC}) {
edges {
node {
title
merged
mergedAt
url
author {
login
url
}
}
}
}
issues(last: 100, orderBy: {field: UPDATED_AT, direction: ASC}) {
edges {
node {
title
closed
updatedAt
url
}
}
}
}
}

How to remove Cartography in iOS

Issue #252

Read more https://medium.com/flawless-app-stories/how-to-make-auto-layout-more-convenient-in-ios-df3b42fed37f

Description

This is a script to remove Cartography, and use plain NSLayoutAnchor syntax.
Use Constraint.on() from Sugar.
It will change all .swift files recursively under provided folder.

1
2
3
4
Constraint.on(
logoImageView.widthAnchor.constraint(equalToConstant: 74),
view1.leftAnchor.constraint(equalTo: view2.leftAnchor, constant: 20),
)

Features

  • Parse recursively
  • Parse a single file
  • Parse each constrain block separately
  • Handle ==, >=, <=
  • Handle center, edges
  • Infer indentation
  • Handle relation with other item
  • Handle relation with constant
  • Handle multiplier
  • Auto import Sugar
  • Infer superView

Prepare

Install tool if needed

1
brew install yarn

How to use

1
2
yarn install
yarn start /Users/khoa/path/to/project

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300

const Glob = require('glob')
const Fs = require('fs')

function main() {
const projectPath = getProjectPath()
const files = getAllFiles(projectPath)

files.forEach((file) => {
handleFile(file)
})
}

/// The 1st argument is node, 2nd is index.js, 3rd is path
function getProjectPath() {
return process.argv[2]
}

/// Only select `swift` file
function getAllFiles(projectPath) {
if (projectPath.endsWith('.swift')) {
return [projectPath]
} else {
const files = Glob.sync(`${projectPath}/**/*.swift`)
return files
}
}

/// Read file, replace by matches, and write again
function handleFile(file) {
console.log(file)

Fs.readFile(file, 'utf8', (error, data) => {
// non greedy `*?`
const pattern = '(.)*constrain\\(.+\\) {\\s(\\s|.)*?\\n(\\s)*}'
const regex = new RegExp(pattern)

let matches = data.match(regex)

// RegEx only return the 1st match per execution, let's do a recursion
while (matches != null) {
const match = matches[0]

const indentationLength = findIndentationLength(match)
const rawTransforms = handleMatch(match)
const transforms = handleSuperview(rawTransforms, match)
const statements = handleTransforms(transforms, indentationLength)
const string = handleStatements(statements, indentationLength)
data = data.replace(match, string)

// Examine again
matches = data.match(regex)
}

// Handle import
data = handleImport(data)


Fs.writeFile(file, data, 'utf8')
})
}

/// Replace or remove import
function handleImport(data) {
if (data.includes('import Sugar')) {
return data.replace('import Cartography', '')
} else {
return data.replace('import Cartography', 'import Sugar')
}
}

/// Format transform to have `,` and linebreaks
function handleTransforms(transforms, indentationLength) {
let string = ''

// Expect the first is always line break
if (transforms[0] != '\n') {
transforms.unshift('\n')
}

const indentation = makeIndentation(indentationLength + 2)

transforms.forEach((line, index) => {
if (line.length > 1) {
string = string.concat(indentation + line)
if (index < transforms.length - 1) {
string = string.concat(',\n')
}
} else {
string = string.concat('\n')
}
})

return string
}

/// Replace superView
function handleSuperview(transforms, match) {
const superView = findSuperview(match)
if (superView == null) {
return transforms
}

const superViewPattern = ' superView.'
transforms = transforms.map((transform) => {
if (transform.includes(superViewPattern)) {
return transform.replace(superViewPattern, ` ${superView}.`)
} else {
return transform
}
})

return transforms
}

/// Embed the statements inside `Constraint.on`
function handleStatements(statements, indentationLength) {
const indentation = makeIndentation(indentationLength)
return `${indentation}Constraint.on(${statements}\n${indentation})`
}

/// Turn every line into flatten transformed line
function handleMatch(match) {
let lines = match.split('\n')
lines = lines.map((line) => {
return handleLine(line.trim())
}).filter((transforms) => {
return transforms != null
})

let flatten = [].concat.apply([], lines)
return flatten
}

/// Check to handle lines, turn them into `LayoutAnchor` statements
function handleLine(line) {
const itemPattern = '\\w*\\.\\w* (=|<|>)= \\w*\\.*\\..*'
const sizePattern = '\w*\.\w* (=|<|>)= (\s|.)*'

if (line.includes('edges == ')) {
return handleEdges(line)
} else if (line.includes('center == ')) {
return handleCenter(line)
} else if (hasPattern(line, itemPattern) && !hasSizeKeywords(line)) {
return handleItem(line)
} else if (hasPattern(line, sizePattern)) {
return handleSize(line)
} else if (line.includes('>=') || line.includes('>=')) {
return line
} else if (line.includes('.')) {
// return the line itself to let the human fix
return line
} else if (line.length == 0) {
return ['\n']
} else {
return null
}
}

/// For ex: listView.bottom == listView.superview!.bottom - 43
/// listView.bottom == listView.superview!.bottom - Metrics.BackButtonWidth
function handleItem(line) {
const equalSign = getEqualSign(line)
const parts = line.split(` ${equalSign} `)
const left = parts[0]

let rightParts = parts[1].trim()
let right = rightParts.split(' ')[0]
let number = rightParts.replace(right, '').replace('- ', '-').trim()
if (number.startsWith('+ ')) {
number = number.slice(2)
}

let equal = getEqual(line)

if (number == null || number.length == 0 ) {
return [
`${left}Anchor.constraint(${equal}: ${right}Anchor)`
]
} else {
return [
`${left}Anchor.constraint(${equal}: ${right}Anchor, constant: ${number})`
]
}
}

/// For ex: segmentedControl.height == 24
/// backButton.width == Metrics.BackButtonWidth
function handleSize(line) {
const equalSign = getEqualSign(line)
const parts = line.split(` ${equalSign} `)
const left = parts[0]
const right = parts[1]

let equal = getEqual(line)

return [
`${left}Anchor.constraint(${equal}Constant: ${right})`
]
}

/// For ex: mapView.edges == listView.edges
function handleEdges(line) {
const parts = line.split(' == ')
const left = parts[0].split('.')[0]
const right = removeLastPart(parts[1])

return [
`${left}.topAnchor.constraint(equalTo: ${right}.topAnchor)`,
`${left}.bottomAnchor.constraint(equalTo: ${right}.bottomAnchor)`,
`${left}.leftAnchor.constraint(equalTo: ${right}.leftAnchor)`,
`${left}.rightAnchor.constraint(equalTo: ${right}.rightAnchor)`
]
}

/// For ex: mapView.center == listView.center
function handleCenter(line) {
const parts = line.split(' == ')
const left = parts[0].split('.')[0]
const right = removeLastPart(parts[1])

return [
`${left}.centerXAnchor.constraint(equalTo: ${right}.centerXAnchor)`,
`${left}.centerYAnchor.constraint(equalTo: ${right}.centerYAnchor)`
]
}

function hasPattern(string, pattern) {
const regex = new RegExp(pattern)
let matches = string.match(regex)

if (matches == null) {
return false
}

matches = matches.filter((match) => {
return match !== undefined && match.length > 1
})

return matches.length > 0
}

function removeLastPart(string) {
const parts = string.split('.')
parts.pop()

return parts.join('.')
}

function getEqual(line) {
if (line.includes('==')) {
return 'equalTo'
} else if (line.includes('<=')) {
return 'lessThanOrEqualTo'
} else {
return 'greaterThanOrEqualTo'
}
}

function getEqualSign(line) {
if (line.includes('==')) {
return '=='
} else if (line.includes('>=')) {
return '>='
} else {
return '<='
}
}

function findIndentationLength(match) {
return match.split('constrain')[0].length + 1
}

function makeIndentation(length) {
return Array(length).join(' ')
}

// For ex: constrain(tableView, headerView, view) { tableView, headerView, superView in
function findSuperview(match) {
const line = match.split('\n')[0]
if (!line.includes(' superView in')) {
return null
}

const pattern = ', \\w*\\)'
const regex = RegExp(pattern)
const string = line.match(regex)[0] // `, view)

return string.replace(', ', '').replace(')', '')
}

// Check special size keywords
function hasSizeKeywords(line) {
const keywords = ['Metrics', 'Dimensions', 'UIScreen']
return keywords.filter((keyword) => {
return line.includes(keyword)
}).length != 0
}

main()

Package node.js application

Issue #57

I like node.js because it has many cool packages. I wish the same goes for macOS. Fortunately, the below solutions provide a way to package node.js modules and use them inside macOS applications. It can be slow, but you save time by using existing node.js modules. Let’s give it a try.

  • pkg Package your Node.js project into an executable 🚀
  • nexe create a single executable out of your node.js apps
  • enclose js Compile your Node.js project into an executable http://enclosejs.com