remove dependency on external github package & exlude mangling of coffeescript node names

This commit is contained in:
Simon Leigh 2019-12-12 17:36:42 +09:00
parent b35e12d508
commit 3c0b659a62
8 changed files with 1024 additions and 310 deletions

973
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@
"start": "electron app/index.js",
"lint": "eslint src/**/*.ts{,x}",
"format": "prettier --write \"src/**/*\"",
"build": "rimraf compiled && env-cmd cross-env TS_NODE_PROJECT=\"tsconfig-webpack.json\" webpack --mode production",
"build": "rimraf compiled && env-cmd cross-env TS_NODE_PROJECT=\"tsconfig-webpack.json\" webpack --mode production --config webpack.production.conf.ts",
"meta": "node scripts/meta.js",
"prepack": "rimraf dist && env-cmd npm run meta && electron-builder --dir",
"pack": "rimraf dist && env-cmd npm run meta && electron-builder",
@ -50,6 +50,7 @@
"@types/react-dom": "^16.9.0",
"@types/react-hot-loader": "^4.1.0",
"@types/shortid": "0.0.29",
"@types/terser-webpack-plugin": "^2.2.0",
"@types/webpack": "^4.39.1",
"@types/webpack-dev-server": "^3.1.7",
"@types/webpack-env": "^1.14.0",
@ -75,6 +76,7 @@
"react-test-renderer": "^16.9.0",
"rimraf": "^3.0.0",
"style-loader": "^1.0.0",
"terser-webpack-plugin": "^2.2.3",
"ts-loader": "^6.2.0",
"ts-node": "^8.4.1",
"type-fest": "^0.7.1",
@ -95,8 +97,8 @@
"aws-amplify": "^1.2.4",
"classcat": "^4.0.2",
"codemirror": "^5.49.0",
"coffeescript": "^2.4.1",
"copy-webpack-plugin": "^5.0.4",
"cson-parser": "git+https://github.com/ButteryCrumpet/cson-parser.git",
"dotenv": "^8.2.0",
"electron-log": "^4.0.0",
"electron-updater": "^4.2.0",

View File

@ -0,0 +1,231 @@
/*
* Copyright (c) 2014, Groupon, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* Neither the name of GROUPON nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { runInThisContext } from 'vm'
import { nodes } from 'coffeescript'
function defaultReviver(_: string, value: any) {
return value
}
function getFunctionNameIE(fn: any) {
return fn.toString().match(/^function\s*([^( ]+)/)[1]
}
function nodeTypeString(csNode: any) {
const ref = csNode.constructor.name
return ref != null ? ref : getFunctionNameIE(csNode.constructor)
}
function syntaxErrorMessage(csNode: any, msg: string) {
const ref = csNode.locationData
const lineIdx = ref.first_line
const columnIdx = ref.first_column
let line = ''
let column = ''
if (lineIdx != null) {
line = lineIdx + 1
}
if (columnIdx != null) {
column = columnIdx + 1
}
return `Syntax error on line ${line}, column ${column}: ${msg}`
}
function parseStringLiteral(literal: string) {
return runInThisContext(literal)
}
function parseRegExpLiteral(literal: string) {
return runInThisContext(literal)
}
function transformKey(csNode: any) {
const type = nodeTypeString(csNode)
if (type !== 'Value') {
throw new SyntaxError(syntaxErrorMessage(csNode, `${type} used as key`))
}
const value = csNode.base.value
switch (value.charAt(0)) {
case "'":
case '"':
return parseStringLiteral(value)
default:
return value
}
}
const nodeTransforms = {
Block: function Block(node: any, transformNode: any) {
const expressions = node.expressions
if (!expressions || expressions.length !== 1) {
throw new SyntaxError(
syntaxErrorMessage(node, 'One top level value expected')
)
}
return transformNode(expressions[0])
},
Value: function Value(node: any, transformNode: any) {
return transformNode(node.base)
},
Bool: function Bool(node: any) {
return node.val === 'true'
},
BooleanLiteral: function BooleanLiteral(node: any) {
return node.value === 'true'
},
Null: function Null() {
return null
},
NullLiteral: function NullLiteral() {
return null
},
Literal: function Literal(node: any) {
const value = node.value
try {
switch (value.charAt(0)) {
case "'":
case '"':
return parseStringLiteral(value)
case '/':
return parseRegExpLiteral(value)
default:
return JSON.parse(value)
}
} catch (error) {
throw new SyntaxError(syntaxErrorMessage(node, error.message))
}
},
NumberLiteral: function NumberLiteral(node: any) {
return Number(node.value)
},
StringLiteral: function StringLiteral(node: any) {
return parseStringLiteral(node.value)
},
RegexLiteral: function RegexLiteral(node: any) {
return parseRegExpLiteral(node.value)
},
Arr: function Arr(node: any, transformNode: any) {
return node.objects.map(transformNode)
},
Obj: function Obj(node: any, transformNode: any, reviver: any) {
return node.properties.reduce((outObject: any, property: any) => {
const variable = property.variable
let value = property.value
if (!variable) {
return outObject
}
const keyName = transformKey(variable)
value = transformNode(value)
outObject[keyName] = reviver.call(outObject, keyName, value)
return outObject
}, {})
},
Op: function Op(node: any, transformNode: any) {
if (node.second != null) {
const left = transformNode(node.first)
const right = transformNode(node.second)
switch (node.operator) {
case '-':
return left - right
case '+':
return left + right
case '*':
return left * right
case '/':
return left / right
case '%':
return left % right
case '&':
return left & right
case '|':
return left | right
case '^':
return left ^ right
case '<<':
return left << right
case '>>>':
return left >>> right
case '>>':
return left >> right
default:
throw new SyntaxError(
syntaxErrorMessage(node, `Unknown binary operator ${node.operator}`)
)
}
} else {
switch (node.operator) {
case '-':
return -transformNode(node.first)
case '~':
return ~transformNode(node.first)
default:
throw new SyntaxError(
syntaxErrorMessage(node, `Unknown unary operator ${node.operator}`)
)
}
}
},
Parens: function Parens(node: any, transformNode: any) {
const expressions = node.body.expressions
if (!expressions || expressions.length !== 1) {
throw new SyntaxError(
syntaxErrorMessage(node, 'Parenthesis may only contain one expression')
)
}
return transformNode(expressions[0])
}
}
export function parse(source: string) {
const reviver = defaultReviver
function transformNode(csNode: any) {
const type = nodeTypeString(csNode)
const transform = nodeTransforms[type]
if (!transform) {
throw new SyntaxError(syntaxErrorMessage(csNode, `Unexpected ${type}`))
}
return transform(csNode, transformNode, reviver)
}
if (typeof reviver !== 'function') {
throw new TypeError('reviver has to be a function')
}
const coffeeAst = nodes(source.toString())
const parsed = transformNode(coffeeAst)
if (reviver === defaultReviver) {
return parsed
}
const contextObj = {}
contextObj[''] = parsed
return reviver.call(contextObj, '', parsed)
}

View File

@ -1,13 +1,19 @@
import { readAsText } from './utils/files'
import { parse } from 'cson-parser'
import { parse } from './cson-parser'
import ow from 'ow'
type ParseErrors = 'read_error' | 'parse_error' | 'not_markdown'
type ParseErrors =
| 'read_error'
| 'cson_parse_error'
| 'not_markdown'
| 'schema_parse_error'
type ParsedNote = {
content: string
tags: string[]
title: string
}
type ConvertResult =
| { err: true; data: ParseErrors }
| { err: false; data: ParsedNote }
@ -23,13 +29,13 @@ export const convertCSONFileToNote = async (
const parsed = await parseCSON(text)
if (parsed === 'parse_error') {
if (parsed === 'cson_parse_error') {
return { err: true, data: parsed }
}
const validated = validateNoteSchema(parsed)
if (validated === 'parse_error') {
if (validated === 'schema_parse_error') {
return { err: true, data: validated }
}
@ -47,14 +53,15 @@ const readFile = (file: File) => readAsText(file).catch(() => 'read_error')
const parseCSON = (text: string) => {
try {
return parse(text)
} catch {
return 'parse_error'
} catch (e) {
console.error(e)
return 'cson_parse_error'
}
}
const validateNoteSchema = (
obj: any
): ParsedNote & { type: string } | 'parse_error' => {
): ParsedNote & { type: string } | 'schema_parse_error' => {
const validator = ow.object.partialShape({
tags: ow.optional.array.ofType(ow.string),
content: ow.string,
@ -66,7 +73,6 @@ const validateNoteSchema = (
ow(obj, validator)
return obj
} catch (e) {
console.log(e)
return 'parse_error'
return 'schema_parse_error'
}
}

View File

@ -2,7 +2,8 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"esModuleInterop": true
"esModuleInterop": true,
"allowJs": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "./typings/**/*.d.ts"]
}

View File

@ -10,7 +10,7 @@
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"allowJs": false,
"allowJs": true,
"checkJs": false,
"lib": ["dom", "es2015", "es2016", "es2017", "esnext"],
"forceConsistentCasingInFileNames": true,

3
typings/coffeescript.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
declare module 'coffeescript' {
export function nodes(input: any): any
}

View File

@ -0,0 +1,92 @@
import path from 'path'
import webpack from 'webpack'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import ErrorOverlayPlugin from 'error-overlay-webpack-plugin'
import CopyPlugin from 'copy-webpack-plugin'
import TerserPlugin from 'terser-webpack-plugin'
module.exports = {
mode: 'production',
entry: [
'./src/index.tsx'
// the entry point of our app
],
output: {
filename: 'bundle.js',
// the output bundle
path: path.resolve(__dirname, 'dist'),
publicPath: '/app'
// necessary for HMR to know where to load the hot update chunks
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
keep_fnames: /Block|Value|Bool|BooleanLiteral|Null|NullLiteral|Literal|NumberLiteral|StringLiteral|RegexLiteral|Arr|Obj|Op|Parens/
}
})
]
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i,
loader: 'url-loader',
options: {
limit: 8192
}
},
{
test: /\.tsx?$/,
use: [{ loader: 'ts-loader' }],
exclude: /node_modules/
},
{
test: /\.md$/,
use: [
{
loader: 'raw-loader'
}
]
}
]
},
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
// do not emit compiled assets that include errors
new HtmlWebpackPlugin(),
new ErrorOverlayPlugin(),
new webpack.EnvironmentPlugin([
'NODE_ENV',
'AMPLIFY_AUTH_IDENTITY_POOL_ID',
'AMPLIFY_AUTH_REGION',
'AMPLIFY_PINPOINT_APPID',
'AMPLIFY_PINPOINT_REGION',
'BOOST_NOTE_BASE_URL'
]),
new CopyPlugin([
{
from: path.join(__dirname, 'node_modules/codemirror/theme'),
to: 'codemirror/theme'
}
])
],
resolve: {
extensions: ['.tsx', '.ts', '.js']
},
node: {
fs: 'empty'
}
}