Does anyone know how to use the javascript-obfuscator (or similar) in Ember ?
I guess it needs to be called inside ember-cli-build.js but I don't know where and how.
Thank you in advance for any help :)
I don't think there's a really straightforward answer to this. If you're running on embroider, then all your Javascript will be handled by webpack, so you could try using webpack-obfuscator -- in your ember-cli-build.js something like
return require('#embroider/compat').compatBuild(app, Webpack, {
plugins: [
new WebpackObfuscator(/*whatever args*/)
],
rules: [
{
test: /\.js$/,
enforce: 'post',
use: {
loader: WebpackObfuscator.loader,
}
}
]
});
The other options I know of would be to write a broccoli plugin. What you're doing is analogous to what ember-cli-terser does -- post-process Javascript files before they get concatenated together -- so you could use that as reference.
You would need to write a broccoli plugin that actually performs the transformations (the analog is broccoli-terser-sourcemap) and an Ember addon to hook it into ember-cli's build pipeline (the analog is ember-cli-terser).
Broccoli plugin
Looking at broccoli-terser-sourcemap's index.js, which is only 114 lines of code, I would think you could adapt it to something like this:
module.exports = class TerserWriter extends Plugin {
constructor(_inputNodes, options = {}) {
let inputNodes = Array.isArray(_inputNodes) ? _inputNodes : [_inputNodes];
super(inputNodes, {
name: options.name,
annotation: options.annotation,
needsCache: false,
});
this.options = defaults(options, {
obfuscator: {},
});
let exclude = this.options.exclude;
if (Array.isArray(exclude)) {
this.excludes = new MatcherCollection(exclude);
} else {
this.excludes = MatchNothing;
}
}
async build() {
let pendingWork = [];
this.inputPaths.forEach(inputPath => {
walkSync(inputPath).forEach(relativePath => {
if (relativePath.slice(-1) === '/') {
return;
}
let inFile = path.join(inputPath, relativePath);
let outFile = path.join(this.outputPath, relativePath);
fs.mkdirSync(path.dirname(outFile), { recursive: true });
if (this._isJSExt(relativePath) && !this.excludes.match(relativePath)) {
pendingWork.push(() => this.processFile(inFile, outFile, relativePath, this.outputPath));
} else {
symlinkOrCopy.sync(inFile, outFile);
}
});
});
for (let fn of pendingWork) {
await fn();
}
}
_isJSExt(relativePath) {
return relativePath.slice(-3) === '.js' || relativePath.slice(-4) === '.mjs';
}
async processFile(inFile, outFile, relativePath, outDir) {
let input = await readFile(inFile).toString();
let result = obfuscate(input, this.options.obfuscator);
await writeFile(outFile, result.getObfuscatedCode());
}
};
You could also do the worker pooling this that broccoli-terser-sourcemaps does, and if you care about source maps you'd need to handle them as well, but broccoli-terser-sourcemaps does just that, so you could use it as reference.
ember-cli addon
ember-cli-terser has even less code -- looking at its index.js, you could adapt it to something like
'use strict';
module.exports = {
name: require('./package').name,
included(app) {
this._super.included.apply(this, arguments);
let defaultOptions = {
enabled: app.env === 'production',
obfuscator: {
// default `javascript-obfuscator` options
},
};
let addonOptions = app.options['ember-cli-obfuscator'];
this._obfuscatorOptions = Object.assign({}, defaultOptions, addonOptions);
},
postprocessTree(type, tree) {
if (this._obfuscatorOptions.enabled === true && type === 'all') {
// Import the plugin code above
const Obfuscator = require('./broccoli-obfuscator');
return new Obfuscator(tree, this._obfuscatorOptions);
} else {
return tree;
}
}
};
Then you'd have to install the above addon in your app (it could be an in-repo addon), and it should do its thing!
This would definitely take some doing, but what you're doing is so similar to what ember-cli-terser is doing, just using the obfuscator API instead of the terser API, that you have a really good starting point.
BUT, if embroider is an option for you, I'd definitely try that route first because it might just be a matter of configuration, rather than writing a bunch of code.
Related
I'm copying all my assets to my build directory using copy webpack plugin. It generates all the files with the hashes properly but is there a simple way of changing any of these file references in code with the new hashed filename?
new CopyWebpackPlugin({
patterns: [
{ from: 'src/assets', to: '[path][name].[contenthash][ext]' },
],
}),
Im currently using strings like below to load assets into phaser, using a query param to break the cache but i want the url to be updated with the new hash filename so I don't need to use the query param to bust the server cache and can take advantage of server caching.
{
key: 'atlas',
url: `assets/img/atlas.json?t=${new Date().getTime().toString()}`,
path: './assets/img/'
},
{
key: 'atlas',
url: `assets/img/atlas.json`,
path: './assets/img/'
},
so Im hoping the above will look like this in the output after webpack has run
{
key: 'atlas',
url: `assets/img/atlas.{generatedHash}.json`,
path: './assets/img/'
},
Edit:
Okay so I accomplished the above by using webpack-asset-manifest
new ManifestPlugin({
// publicPath: 'assets/',
}),
and then having a function for getting my urls from the generated json after it is loaded
protected getAssetFromKey(key: string | Array<string>): string | Array<string> {
if (Array.isArray(key)) {
let urls = new Array<string>();
key.forEach((urlKey: string) => {
urls.push(this.assetKeys[urlKey]);
});
return urls;
} else {
return this.assetKeys[key];
}
}
But now I have hit an issue where the json atlas files are pointing to the old images and there seems to be no easy way to edit these. Im thinking of something like string replace loader but Im wondering if there is a better way and I am unsure of how to replace the string with a value from the manifest.json that is exported by webpack.
Okay So I figured this out by adding the manifest plugin to my webpack config which generates a json file with all the original file names as keys and all the new hashed filenames as values.
new ManifestPlugin({
}),
and then I added in a compiler hook in the plugins area that does reads all the atlas files and replaces the strings in them, as well as any references to the assets in the compiled code.
https://webpack.js.org/api/compiler-hooks/
{
apply: (compiler) => {
compiler.hooks.done.tap("update links", (stats) => {
Fs.readJson('dist/assets-manifest.json').then( (value) => {
let keys = Object.keys(value)
let files = Object.values(value);
files.forEach( (file, index) => {
if( file.includes('json')) {
let splitString = keys[index].split('/');
let findFile = splitString[splitString.length-1].replace('.json', '');
console.log(`find file- ${findFile}`);
let replaceWithString = '';
let replaceString = ''
for( let i =0 ; i < keys.length; i++) {
if( keys[i].includes(`${findFile}`) && keys[i].includes('.webp') ) {
console.log(keys[i]);
let splitFiles = files[i].split('/');
let splitKeys = keys[i].split('/');
replaceWithString = splitFiles[splitFiles.length-1];
replaceString = splitKeys[splitKeys.length-1];
break;
}
}
console.log( `REPLACE WITH STRING = ${replaceWithString}`)
console.log(`reading file-${file}`);
Fs.readJson(`dist/${file}`).then( (val) => {
let stringJson = JSON.stringify(val);
console.log(`replacing ${replaceString} with ${replaceWithString}`);
let result = stringJson.replace(replaceString, replaceWithString);
let outputJson = JSON.parse(result);
Fs.writeJson(`dist/${file}`, outputJson, 'utf8').then( () => {
console.log( `!!!!! SUCCESS !!!!!`);
});
});
}
});
files.forEach( (file) => {
if( file.includes('.js') && !file.includes('json') ) {
console.log('FILE: ' + file)
Fs.content(`dist/${file}`).then( ( val) => {
keys.forEach( (key,index) => {
if( key.includes('assets/')) {
val = val.replaceAll(key, files[index]);
console.log(`REPLACING: ${key} with ${files[index]} in ${file}`)
}
});
Fs.writeFile(`dist/${file}`, val).then( () => {
console.log("--SUCCESS---")
});
});
}
})
}).catch( (err) => {
console.log(`error ${err}`);
})
});
}
},
I want to enforce one-line if statements not having braces. For example:
// incorrect
if(foo) {
bar()
}
// correct
if(foo) bar()
But if I have else/else ifs, I still want to keep braces:
// incorrect
if(foo) bar()
else(baz) qux()
// correct
if(foo) {
bar()
} else(baz) {
qux()
}
I was able to create a plugin like this:
module.exports = {
meta: {
type: "layout",
fixable: "whitespace",
messages: {
noSingleLineIfsAsBlock: "Don't use braces with one line if statements",
useBlocksWithElseCases: "Use braces when there are else ifs and/or elses"
}
},
create(context) {
return {
IfStatement(node) {
if(node.alternate === null && node.parent.type !== "IfStatement") {
if(node.consequent.type === "BlockStatement" && node.consequent.body.length > 0) {
// assumes that blank lines are removed by other eslint rules, so at most three lines for a if block with one line inside
if(node.consequent.loc.end.line - node.consequent.loc.start.line + 1 <= 3) {
context.report({
node: node,
messageId: "noSingleLineIfsAsBlock",
fix(fixer) {
const sourceCode = context.getSourceCode();
const openingBrace = sourceCode.getFirstToken(node.consequent);
const closingBrace = sourceCode.getLastToken(node.consequent);
const firstValueToken = sourceCode.getFirstToken(node.consequent.body[0]);
const lastValueToken = sourceCode.getLastToken(node.consequent.body[0]);
return [
fixer.removeRange([openingBrace.range[0], firstValueToken.range[0]]),
fixer.removeRange([lastValueToken.range[1], closingBrace.range[1]])
];
}
})
}
}
} else if(node.alternate || node.parent.type === "IfStatement") {
if(node.consequent.type !== "BlockStatement") {
context.report({
node: node,
messageId: "useBlocksWithElseCases",
fix(fixer) {
// assumes that other eslint rules will fix brace styling
const sourceCode = context.getSourceCode();
const firstValueToken = sourceCode.getFirstToken(node.consequent);
const lastValueToken = sourceCode.getLastToken(node.consequent);
return [
fixer.insertTextBefore(firstValueToken, "{"),
fixer.insertTextAfter(lastValueToken, "}")
];
}
})
}
if(node.alternate && node.alternate.type !== "IfStatement" && node.alternate.type !== "BlockStatement") {
context.report({
node: node,
messageId: "useBlocksWithElseCases",
fix(fixer) {
// assumes that other eslint rules will fix brace styling
const sourceCode = context.getSourceCode();
const firstValueToken = sourceCode.getFirstToken(node.alternate);
const lastValueToken = sourceCode.getLastToken(node.alternate);
return [
fixer.insertTextBefore(firstValueToken, "{"),
fixer.insertTextAfter(lastValueToken, "}")
];
}
})
}
}
}
};
}
};
But I was wondering if there are already existing implementations I could use instead so I wouldn't have to store this in my project.
I browsed npm packages relating to eslint and searched for things like "eslint if statement formatting" but I couldn't find anything. I'm also pretty sure eslint doesn't have a built-in rule for this.
The most related issue I found was this, but it's asking how to avoid the style I'm trying to implement, and I'm not using prettier.
Are there any eslint plugins like this that can solve my problem?
I recently have spotted jscodeshift and leveraging it for refactoring.
But I can't find a way to add await before the node that I am seeking.
// AS-IS
userEvent.click(a);
await userEvent.click(b);
// TO-BE
await userEvent.click(a);
await userEvent.click(b);
This is how I query userEvent.click
const getFunctionCall = (obj:string, prop:string) => {
return source.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
object: {
name: obj
},
property: {
name:prop
}
}
})
}
const clicks = getFunctionCall('userEvent', 'click');
clicks.forEach(i=> {
if (i.parentPath.value.type !== 'AwaitExpression') {
// code goes here
}
})
How can I add await before userEvent and let the rest code stay same in this case?
I checked out the document to find out how to build a statement or expression, but it wasn't understandable for me as I've just started to use this. I would appreciate if you can introduce any good materials for this library.
Thanks in advance!
It is simple, you need to wrap the existing CallExpression inside an await expression like this:
// Press ctrl+space for code completion
export default function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
const body = root.get().value.program.body;
root.find(j.CallExpression, {
callee: {
object: {
name: "userEvent"
},
property: {
name: "click"
}
}
}).filter(path => {
return path.value.arguments[0].name === "a" ;
}).replaceWith(path => {
return j.awaitExpression(path.value)
});
return root.toSource();
}
How can I run additional assertion after Botium Binding is completed?
const bb = new BotiumBindings({ convodirs: ['/spec'] })
BotiumBindings.helper.mocha().setupMochaTestSuite({ bb })
// TODO: GET request to an external API.
The recommended way to add your custom asserter logic to Botium is by adding your own asserter module to Botium. You can find the details here, but in short:
Create a file MyCustomAsserter.js:
module.exports = class CustomAsserter {
constructor (context, caps, globalArgs) {
this.context = context
this.caps = caps
this.globalArgs = globalArgs
}
assertConvoStep ({convo, convoStep, args, isGlobal, botMsg}) {
if (botMsg.message !== 'hugo') throw new Error('expected hugo')
return Promise.resolve()
}
}
Register it in botium.json
{
"botium": {
"Capabilities": {
...
"ASSERTERS": [
{
"ref": "MY-ASSERTER-NAME",
"src": "./MyCustomAsserter.js",
"global": false,
"args": {
"my-arg-1": "something"
}
}
]
}
}
}
Use this asserter in your convo files:
my-test-case
#me
hi
#bot
hi
MY-ASSERTER-NAME some-arg1|some-arg2
I like to implement like a namespacing something like this as example:
const { SMS } = require('./custom-sdk')
const list = SMS.List();
let data = list.getData("ABC");
console.log(data)
I am completely stuck how to implement this, what do I need to do design this kind of API methods.
I have tried like this would which would be in custom-sdk.js file:
module.exports = {
SMS: function() {
// ...
}
};
Would something like the following nesting work?
module.exports = {
SMS: {
List: function() {
return {
getData: function(arg) {
// get that data
}
}
}
}
};
Which I think would allow you to do SMS.List().getData('ABC'). That said, this seems overly nested, unless you just simplified it for the SO question. I would suggest to only use functions when necessary (to take an argument or to instantiate a service) and prefer just a plain object when possible:
module.exports = {
SMS: {
List: {
getData: function(arg) {
// get that data
}
}
}
};