I'm developing an app in VueJS 3 and it seems that's working on my local computer.
But I have to make it runs also on older browsers like Firefox 38 Chrome 49.
My app uses some "fetch" functions to load content from api, to authenticate, to send commands, etc. After reading the following 2 links I believe I have some issues with it (despite it should works on CH 42). https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API and Babel not Polyfilling Fetch when using babel-preset-env
. What it is your opinion?
I have babel and in package.json the following settings:
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"Chrome > 48",
]
But the errors received in Firefox and Chrome are very cryptic and I don't know what how should I transpile that function. I would focus on Chrome 49 and I'll add some context
Indeed the async function is available after Chrome 55. But I don't know how to convert it
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
For example in Chrome 49 I have the following console error on the following page: webpack:///./node_modules/#vue/devtools-api/esm/proxy.js
Uncaught SyntaxError: Unexpected identifier proxy.js:94
The content of that file is:
import { HOOK_PLUGIN_SETTINGS_SET } from './const';
export class ApiProxy {
constructor(plugin, hook) {
this.target = null;
this.targetQueue = [];
this.onQueue = [];
this.plugin = plugin;
this.hook = hook;
const defaultSettings = {};
if (plugin.settings) {
for (const id in plugin.settings) {
const item = plugin.settings[id];
defaultSettings[id] = item.defaultValue;
}
}
const localSettingsSaveId = `__vue-devtools-plugin-settings__${plugin.id}`;
let currentSettings = Object.assign({}, defaultSettings);
try {
const raw = localStorage.getItem(localSettingsSaveId);
const data = JSON.parse(raw);
Object.assign(currentSettings, data);
}
catch (e) {
// noop
}
this.fallbacks = {
getSettings() {
return currentSettings;
},
setSettings(value) {
try {
localStorage.setItem(localSettingsSaveId, JSON.stringify(value));
}
catch (e) {
// noop
}
currentSettings = value;
},
};
if (hook) {
hook.on(HOOK_PLUGIN_SETTINGS_SET, (pluginId, value) => {
if (pluginId === this.plugin.id) {
this.fallbacks.setSettings(value);
}
});
}
this.proxiedOn = new Proxy({}, {
get: (_target, prop) => {
if (this.target) {
return this.target.on[prop];
}
else {
return (...args) => {
this.onQueue.push({
method: prop,
args,
});
};
}
},
});
this.proxiedTarget = new Proxy({}, {
get: (_target, prop) => {
if (this.target) {
return this.target[prop];
}
else if (prop === 'on') {
return this.proxiedOn;
}
else if (Object.keys(this.fallbacks).includes(prop)) {
return (...args) => {
this.targetQueue.push({
method: prop,
args,
resolve: () => { },
});
return this.fallbacks[prop](...args);
};
}
else {
return (...args) => {
return new Promise(resolve => {
this.targetQueue.push({
method: prop,
args,
resolve,
});
});
};
}
},
});
}
async setRealTarget(target) {
this.target = target;
for (const item of this.onQueue) {
this.target.on[item.method](...item.args);
}
for (const item of this.targetQueue) {
item.resolve(await this.target[item.method](...item.args));
}
}
}
I tried to change anything I found:
vue.config.js
transpileDependencies: [
"config",
"vue",
"vue-router",
"vuex",
"xml2js"
],
package.json
"dependencies": {
"#babel/polyfill": "^7.12.1",
"babel-polyfill": "^6.26.0",
"config": "^3.3.7",
"core-js": "^3.6.5",
"regenerator-runtime": "^0.13.9",
"vue": "^3.2.26",
"vue-router": "^4.0.12",
"vuex": "^4.0.2",
"xml2js": "^0.4.23"
},
"devDependencies": {
"#babel/cli": "^7.16.8",
"#babel/core": "^7.16.12",
"#babel/preset-env": "^7.16.11",
"#vue/babel-preset-app": "^4.5.15",
"#vue/cli-plugin-babel": "~4.5.0",
"#vue/cli-plugin-eslint": "~4.5.0",
"#vue/cli-plugin-router": "~4.5.0",
"#vue/cli-service": "~4.5.0",
"#vue/compiler-sfc": "^3.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.8.0",
"eslint-plugin-vue": "^7.0.0"
},
"browserslist": [
"defaults",
"> 1%",
"last 2 versions",
"not dead",
"Chrome > 48"
]
babel.config.js
module.exports = {
presets: [
[
'#vue/cli-plugin-babel/preset',
{
useBuiltIns: "usage",
forceAllTransforms: true,
targets: {
"chrome": "49"
},
}
]
]
};
main.js
import "#babel/polyfill";
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './_store'
createApp(App).use(router).use(store).mount('#app');
i setup a new vue 3 project and added async/await and fetch and did a build and it workes in chrome 48
I modified hello world component to
<script>
export default {
name: 'HelloWorld',
data() {
return {msg : ""}
},
mounted() {
async function fetchMoviesJSON() {
const response = await fetch('https://reqbin.com/echo/get/json');
const movies = await response.json();
return movies;
}
fetchMoviesJSON().then(data => {
this.msg = data; // fetched movies
});
}
}
</script>
added "Chrome > 47" to browserslist
also i installed browserslist
can you run after installing browserslist npx browserslist and check if you see chrome 47 there?
I saw in your error dev-tools i think it might be just the Vue dev tools
Update:
added vue router and installed the vue dev tools and when i run with serve i can see that the proxy file was compiled there and no errors.
are you building it in dev mode?
I would recommend you to clean up your babel plugins and try with the simple way.
also please let know the node/nom version. i read it might be the problem, and you should try to update node see here
Related
Hi im trying to create tests for my code and i got a problem, the main message on terminal is:
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest).
{import * as strtok3 from 'strtok3';
^^^^^^
SyntaxError: Cannot use import statement outside a module
looks that one of my dependencies is unable to work with jest, maybe cause im using typescript. Someone knows how can i fix that? I let all necessary info about my project, including the error on terminal, above. Thank you.
File that im testing:
import {
S3Client,
PutObjectCommand,
PutObjectCommandInput,
} from "#aws-sdk/client-s3";
import { v4 as uuidv4 } from "uuid";
import { fileTypeFromBuffer } from "file-type";
import { MulterFile } from "../../../types/MulterFile";
import { createImageInDB } from "../create-in-db";
const client = new S3Client({
region: "sa-east-1",
credentials: {
accessKeyId: process.env.AWS_IAM_IMAGES_ACCESS_KEY!,
secretAccessKey: process.env.AWS_IAM_IMAGES_SECRET_KEY!,
},
});
// export const UploadFileSchema = z.object({
// file: z.instanceof(),
// });
// export type UploadFileType = z.infer<typeof UploadFileSchema>;
export const verifyFileTypeIsCorrect = async (
types: string[],
file: MulterFile
) => {
const fileType = await fileTypeFromBuffer(file.buffer);
if (!fileType) {
return false;
}
if (types.includes(fileType.ext)) {
return `.${fileType.ext}`;
}
return false;
};
const acceptedFileTypes = ["jpeg", "png", "jpg", "webp"];
export const uploadImage = async ({
file,
path,
userId,
}: {
file: MulterFile;
path: string;
userId?: string | null;
}) => {
const fileType = await verifyFileTypeIsCorrect(acceptedFileTypes, file);
if (!fileType) {
return {
success: false,
message: `Tipo de arquivo inválido, aceitamos apenas: ${acceptedFileTypes.join(
", "
)}`,
};
}
const newFileName =
`${path}/${userId ? userId + "/" : ""}` + uuidv4() + fileType;
const uploadParams: PutObjectCommandInput = {
Bucket: process.env.AWS_IAM_IMAGES_BUCKET_NAME!,
Key: newFileName,
Body: file.buffer,
};
let output;
try {
output = await client.send(new PutObjectCommand(uploadParams));
} catch (error) {
return {
success: false,
message: "Erro ao fazer upload da imagem.",
};
}
const url = `https://${process.env.AWS_IAM_IMAGES_BUCKET_NAME}.s3.sa-east-1.amazonaws.com/${newFileName}`;
return {
success: true,
message: "Upload feito com sucesso.",
data: {
url,
},
};
};
export const uploadImageAndCreateInDB = async ({
file,
path,
userId,
alt,
name,
}: {
file: MulterFile;
path: string;
userId: string;
alt?: string;
name?: string;
}) => {
const {
data: dataUpload,
message: messageUpload,
success: successUpload,
} = await uploadImage({ file, path, userId });
if (!successUpload || !dataUpload) {
throw new Error(messageUpload);
}
const { data, message, success } = await createImageInDB({
url: dataUpload.url,
alt,
userId,
name,
});
if (!success || !data) {
throw new Error(message);
}
return {
success: true,
message: "Upload feito com sucesso.",
data,
};
};
jest.config.js
module.exports = {
clearMocks: true,
preset: "ts-jest",
testEnvironment: "node",
setupFilesAfterEnv: ["./singleton.ts"],
};
package.json
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"postinstall": "prisma generate",
"test": "NODE_ENV=test jest -i --verbose"
},
"dependencies": {
"#aws-sdk/client-s3": "^3.238.0",
"#bmunozg/react-image-area": "^1.1.0",
"#headlessui/react": "^1.7.7",
"#heroicons/react": "^2.0.13",
"#next-auth/prisma-adapter": "^1.0.5",
"#nivo/bar": "^0.80.0",
"#nivo/core": "^0.80.0",
"#nivo/line": "^0.80.0",
"#prisma/client": "^4.8.0",
"#sendgrid/mail": "^7.7.0",
"#stripe/stripe-js": "^1.46.0",
"#tanstack/react-query": "^4.20.4",
"#trpc/client": "^10.7.0",
"#trpc/next": "^10.7.0",
"#trpc/react-query": "^10.7.0",
"#trpc/server": "^10.7.0",
"#types/node": "18.11.9",
"#types/react": "18.0.25",
"bcrypt": "^5.1.0",
"blaze-slider": "^1.9.0",
"cookies": "^0.8.0",
"daisyui": "^2.46.0",
"eslint": "8.27.0",
"eslint-config-next": "^13.1.1",
"file-type": "^18.0.0",
"jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21",
"micro": "^9.4.1",
"micro-cors": "^0.1.1",
"multer": "^1.4.5-lts.1",
"next": "^13.1.1",
"next-auth": "^4.18.7",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-toastify": "^9.1.1",
"sanitize-html": "^2.8.1",
"stripe": "^11.5.0",
"uuid": "^9.0.0",
"zod": "^3.20.2"
},
"devDependencies": {
"#tailwindcss/typography": "^0.5.8",
"#types/bcrypt": "^5.0.0",
"#types/cookies": "^0.7.7",
"#types/jest": "^29.2.4",
"#types/jsonwebtoken": "^8.5.9",
"#types/lodash": "^4.14.191",
"#types/micro-cors": "^0.1.2",
"#types/multer": "^1.4.7",
"#types/react-dom": "^18.0.10",
"#types/sanitize-html": "^2.8.0",
"#types/uuid": "^8.3.4",
"autoprefixer": "^10.4.13",
"jest": "^29.3.1",
"jest-mock-extended": "^3.0.1",
"postcss": "^8.4.20",
"prisma": "^4.8.0",
"tailwindcss": "^3.2.4",
"ts-jest": "^29.0.3",
"typescript": "^4.9.4"
}
index.test.ts file:
import {
uploadImage,
verifyFileTypeIsCorrect,
} from "../../../src/lib/services/image/upload";
import { OBJECT_TO_MOCK_USER } from "../auth/index.test";
export const OBJECT_TO_MOCK_IMAGE = () => ({
id: "clbfokijd00007z6bsf3qgq5t",
url: `https://${process.env.AWS_IAM_IMAGES_BUCKET_NAME}.s3.sa-east-1.amazonaws.com/texts/clbfp20sl00097z6bnuptsu8f/b49afd22-337b-43e9-811c-d70c551c4086.jpg`,
name: "Teste Nome",
});
jest.mock("#aws-sdk/client-s3", () => {
return {
S3Client: jest.fn(() => ({
send: jest.fn(() => ({
success: true,
message: "Upload feito com sucesso.",
data: {
url: `https://${process.env.AWS_IAM_IMAGES_BUCKET_NAME}.s3.sa-east-1.amazonaws.com/texts/clbfp20sl00097z6bnuptsu8f/b49afd22-337b-43e9-811c-d70c551c4086.jpg`,
},
})),
})),
};
});
jest.mock("uuid", () => {
return {
v4: jest.fn(() => "b49afd22-337b-43e9-811c-d70c551c4086"),
};
});
describe("Image", () => {
let image: any;
let user: any;
beforeEach(async () => {
image = OBJECT_TO_MOCK_IMAGE();
user = OBJECT_TO_MOCK_USER("123456789");
});
test("deve verificar se os tipos do arquivo são válidos", async () => {
const file = {
mimetype: "application/pdf",
buffer: Buffer.from(""),
originalname: "teste.pdf",
};
const acceptedFileTypes = ["jpeg", "png", "jpg", "webp"];
await expect(
verifyFileTypeIsCorrect(acceptedFileTypes, file)
).resolves.toEqual({
success: false,
message: `Tipo de arquivo inválido, aceitamos apenas: ${acceptedFileTypes.join(
", "
)}`,
});
});
test("verify uploadImage function que deve fazer upload de uma imagem", async () => {
const file = {
mimetype: "image/jpeg",
buffer: Buffer.from(""),
originalname: "teste.jpeg",
};
await expect(
uploadImage({
file,
path: "teste",
userId: user.id,
})
).resolves.toEqual({
success: true,
message: "Upload feito com sucesso.",
data: {
url: `https://${process.env.AWS_IAM_IMAGES_BUCKET_NAME}.s3.sa-east-1.amazonaws.com/teste/${user.id}/b49afd22-337b-43e9-811c-d70c551c4086.jpg`,
},
});
});
});
Console Error:
FAIL tests/api/image/index.test.ts
● Test suite failed to run
Jest encountered an unexpected token
Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.
Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.
By default "node_modules" folder is ignored by transformers.
Here's what you can do:
• If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
• If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/configuration
For information about custom transformations, see:
https://jestjs.io/docs/code-transformation
Details:
/Users/test/Dev/next/node_modules/file-type/index.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import * as strtok3 from 'strtok3';
^^^^^^
SyntaxError: Cannot use import statement outside a module
5 | } from "#aws-sdk/client-s3";
6 | import { v4 as uuidv4 } from "uuid";
> 7 | import { fileTypeFromBuffer } from "file-type";
| ^
8 | import { MulterFile } from "../../../types/MulterFile";
9 | import { createImageInDB } from "../create-in-db";
10 |
at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1449:14)
at Object.<anonymous> (src/lib/services/image/upload/index.ts:7:1)
at Object.<anonymous> (tests/api/image/index.test.ts:4:1)
According to another post on stackoverflow: "Uncaught SyntaxError: Cannot use import statement outside a module" when importing ECMAScript 6
You can add
"type": "module",
to your package.json.
Hope this helps!
My electron app is using the remote isolation approach (mentioned in this article). My remote web app is a Ruby on Rails app. Now, I want to push notifications from a remote web app to the electron app using WebSocket. To send the message to users even if they shut off the app, I got an idea. In the Rails server, I use ActionCable to broadcast the notifications to a channel. In the main process, I subscribe to this channel by using the actioncable package.
import ActionCable from "actioncable";
const websocketUrl = "wss://rt.custom-domain.dev/_/cable";
const cable = ActionCable.createConsumer(websocketUrl);
cable.subscriptions.create(
{ channel: "WallChannel", wall_id: "106" },
{
received(data: any): void {},
disconnect(): void {},
connected(): void {},
disconnected(): void {},
present(): void {},
absent(): void {},
}
);
But I got an error:
ReferenceError: window is not defined
Then I dive deep into the source code of the actioncable package and I found that the package using WebSocket API of the browser so that why the error appeared.
I tried to use ws package to subscribe to the websocket instead by following this post. But I couldn't connect to the websocket server. When I call App.ws.sendmessage I got an error:
Error: WebSocket is not open: readyState 0 (CONNECTING)
Has anyone tried to push notifications from the remote web app by using websocket like what I've trying? Did you got the same problem? or If you got a better solution for my case, please share with me your idea. Thanks a lot.
This is my main.ts file of the electron app
import { app, BrowserWindow, ipcMain, Notification } from "electron";
import { createWindow } from "src/main/CreateWindow";
import { showNotification } from "./helpers";
import appConfigs from "src/AppConfigs";
let mainWindow: BrowserWindow;
const createAppWindow = () => {
mainWindow = createWindow(appConfigs.targetUrl, { interop: true });
// Open the DevTools.
if (!app.isPackaged) {
mainWindow.webContents.openDevTools();
}
};
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require("electron-squirrel-startup")) {
// eslint-disable-line global-require
app.quit();
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on("ready", () => {
createAppWindow();
});
var App: { ws: any; param: any; connect_server: any; sendmessage: any } = {
ws: null,
param: null,
connect_server: () => {},
sendmessage: () => {},
};
App.sendmessage = function (send: any) {
let data = {
message: send,
action: "speak",
};
let message = {
command: "message",
identifier: JSON.stringify(App.param),
data: JSON.stringify(data),
};
App.ws.send(JSON.stringify(message));
};
App.connect_server = function () {
const WebSocket = require("ws");
App.ws = new WebSocket("wss://rt.custom-domain.dev/_/cable", [
"actioncable-v1-json",
"actioncable-unsupported",
]);
console.log("connect_server", App.ws);
App.param = { channel: "WallChannel", wall_id: 106 };
App.ws.on("open", function open() {
console.log("open channel");
let data = {
command: "subscribe",
identifier: JSON.stringify(App.param),
};
App.ws.send(JSON.stringify(data));
console.log("send JSON");
});
App.ws.on("message", function (event: any) {
console.log("message", event);
});
App.ws.on("error", function (err) {
console.log("Found error: " + err);
});
};
App.connect_server();
CreateWindow.ts
import { BrowserWindow } from "electron";
import appConfigs from "src/AppConfigs";
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: any;
interface BrowserWindowOption {
title?: string;
height?: string;
width?: string;
interop?: boolean;
}
export const createWindow = (
url: string,
options: BrowserWindowOption = {}
): BrowserWindow => {
// Create the browser window.
const mainWindow = new BrowserWindow({
title: options.title || appConfigs.name,
height: options.height || appConfigs.height,
width: options.width || appConfigs.width,
webPreferences: options.interop
? {
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
}
: {},
});
// and load the targetUrl.
mainWindow.loadURL(url || appConfigs.targetUrl);
return mainWindow;
};
package.json
{
"name": "electron-forge-app",
"productName": "electron-forge-app",
"version": "1",
"description": "My Electron application description",
"main": ".webpack/main",
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "eslint --ext .ts ."
},
"keywords": [],
"author": {
...
},
"license": "MIT",
"config": {
"forge": {
"packagerConfig": {},
"makers": [
{
"name": "#electron-forge/maker-squirrel",
"config": {
"name": "electron_furge"
}
},
{
"name": "#electron-forge/maker-zip",
"platforms": [
"darwin"
]
},
{
"name": "#electron-forge/maker-deb",
"config": {}
},
{
"name": "#electron-forge/maker-rpm",
"config": {}
}
],
"plugins": [
[
"#electron-forge/plugin-webpack",
{
"mainConfig": "./webpack.main.config.js",
"renderer": {
"config": "./webpack.renderer.config.js",
"entryPoints": [
{
"html": "./src/index.html",
"js": "./src/renderer.ts",
"name": "main_window",
"preload": {
"js": "./src/interop/preload.ts"
}
}
]
}
}
]
]
}
},
"devDependencies": {
"#electron-forge/cli": "^6.0.0-beta.54",
"#electron-forge/maker-deb": "^6.0.0-beta.54",
"#electron-forge/maker-rpm": "^6.0.0-beta.54",
"#electron-forge/maker-squirrel": "^6.0.0-beta.54",
"#electron-forge/maker-zip": "^6.0.0-beta.54",
"#electron-forge/plugin-webpack": "6.0.0-beta.54",
"#marshallofsound/webpack-asset-relocator-loader": "^0.5.0",
"#types/actioncable": "^5.2.4",
"#typescript-eslint/eslint-plugin": "^4.0.1",
"#typescript-eslint/parser": "^4.0.1",
"css-loader": "^4.2.1",
"electron": "12.0.5",
"eslint": "^7.6.0",
"eslint-plugin-import": "^2.20.0",
"fork-ts-checker-webpack-plugin": "^5.0.14",
"node-loader": "^1.0.1",
"style-loader": "^1.2.1",
"ts-loader": "^8.0.2",
"typescript": "^4.0.2"
},
"dependencies": {
"actioncable": "^5.2.6",
"electron-squirrel-startup": "^1.0.0",
"ws": "^7.4.5"
}
}
I followed this tutorial: https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react
From the beginning, my app was extremely slow to load, including when changing tabs, including when loaded through ngrok and run on localhost or deployed on app engine.
What could be causing this ?
P.S.: I am new to React, Next.js and Shopify App development, so the answer could be quite basic.
P.P.S.: The build output seems to indicate "First Load JS shared by all" is too large based on the red color. I do not know how to investigate this and reduce the size of said chunks although a mere 214KB could not explain such slow load times, could it ?
Build
React Dev Tools Profiler
#next/bundle-analyzer Output:
Parsed
Gzipped
package.json
{
"name": "ShopifyApp1",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "node server.js NODE_ENV=dev",
"build": "next build",
"deploy": "next build && gcloud app deploy --version=deploy",
"start": "NODE_ENV=production node server.js",
"analyze": "cross-env ANALYZE=true npm run build"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"#google-cloud/storage": "^5.2.0",
"#next/bundle-analyzer": "^9.5.2",
"#sendgrid/mail": "^7.2.3",
"#shopify/app-bridge-react": "^1.26.2",
"#shopify/koa-shopify-auth": "^3.1.65",
"#shopify/koa-shopify-graphql-proxy": "^4.0.1",
"#shopify/koa-shopify-webhooks": "^2.4.3",
"#shopify/polaris": "^5.1.0",
"#zeit/next-css": "^1.0.1",
"apollo-boost": "^0.4.9",
"cors": "^2.8.5",
"cross-env": "^7.0.2",
"dotenv": "^8.2.0",
"email-validator": "^2.0.4",
"extract-domain": "^2.2.1",
"firebase-admin": "^9.0.0",
"graphql": "^15.3.0",
"helmet": "^4.0.0",
"isomorphic-fetch": "^2.2.1",
"js-cookie": "^2.2.1",
"koa": "^2.13.0",
"koa-body": "^4.2.0",
"koa-bodyparser": "^4.3.0",
"koa-helmet": "^5.2.0",
"koa-router": "^9.1.0",
"koa-session": "^6.0.0",
"next": "^9.5.1",
"react": "^16.13.1",
"react-apollo": "^3.1.5",
"react-dom": "^16.13.1",
"react-infinite-scroll-component": "^5.0.5",
"sanitize-html": "^1.27.2",
"scheduler": "^0.19.1",
"store-js": "^2.0.4",
"tldts": "^5.6.46"
},
"devDependencies": {
"webpack-bundle-analyzer": "^3.8.0",
"webpack-bundle-size-analyzer": "^3.1.0"
},
"browser": {
"#google-cloud/storage": false,
"#sendgrid/mail": false,
"#shopify/koa-shopify-auth": false,
"#shopify/koa-shopify-graphql-proxy": false,
"#shopify/koa-shopify-webhooks": false,
"cors": false,
"email-validator": false,
"extract-domain": false,
"firebase-admin": false,
"graphql": false,
"helmet": false,
"isomorphic-fetch": false,
"koa": false,
"koa-body": false,
"koa-bodyparser": false,
"koa-helmet": false,
"koa-router": false,
"koa-session": false,
"sanitize-html": false,
"tldts": false
}
}
Chrome Dev Tools Network Tab
EDIT:
npm run dev
For some reason, the "webpack-hmr" line load time keeps constantly increasing.
npm run build && npm run start
next.config.js
require("dotenv").config({path:"live.env"});
const withCSS = require('#zeit/next-css');
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const withBundleAnalyzer = require('#next/bundle-analyzer')({enabled: process.env.ANALYZE === 'true'})
const apiKey = JSON.stringify(process.env.SHOPIFY_API_KEY);
module.exports = withBundleAnalyzer(
withCSS({
distDir: 'build',
webpack: (config) => {
const env = { API_KEY: apiKey };
config.plugins.push(new webpack.DefinePlugin(env));
config.plugins.push(new webpack.DefinePlugin(new BundleAnalyzerPlugin()));
config.resolve = {
alias: {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling'
},
...config.resolve
};
return config;
}
})
);
_app.js
import App from 'next/app';
import Head from 'next/head';
import { AppProvider } from '#shopify/polaris';
import { Provider } from '#shopify/app-bridge-react';
import '#shopify/polaris/dist/styles.css'
import translations from '#shopify/polaris/locales/en.json';
import Cookies from 'js-cookie';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';
const client = new ApolloClient({
fetchOptions: {
credentials: 'include'
},
});
class MyApp extends App {
render() {
const { Component, pageProps } = this.props;
const config = { apiKey: API_KEY, shopOrigin: Cookies.get("shopOrigin"), forceRedirect: true };
return (
<React.Fragment>
<Head>
<title>...</title>
<meta charSet="utf-8" />
</Head>
<Provider config={config}>
<AppProvider i18n={translations}>
<ApolloProvider client={client}>
<Component {...pageProps} />
</ApolloProvider>
</AppProvider>
</Provider>
</React.Fragment>
);
}
}
export default MyApp;
Index.js (client)
import {
Button,
Card,
Form,
FormLayout,
Layout,
Page,
Stack,
TextField,
DisplayText,
Toast,
Frame
} from '#shopify/polaris';
class Index extends React.Component {
state = {
emails: '',
domain: '' ,
alias: '',
err: '',
message: '',
active: false,
loadingDomainResponse: false,
loadingEmailResponse: false
};
componentDidMount() {
fetch(`/state`, {
method: 'GET'
}).then(response => response.json())
.then(result => {
if (result.err) {
this.setState({
err: result.err,
message: result.err,
active: true
})
}
else {
this.setState({
emails: result.emails,
domain: result.domain,
alias: result.alias
})
}
});
};
render() {
const { emails, domain, alias, err, message, active, loadingEmailResponse, loadingDomainResponse} = this.state;
const toastMarkup = active ? (
<Toast content={message} error={err} onDismiss={this.handleToast}/>
) : null;
return (
<Frame>
<Page>
{toastMarkup}
<Layout>
<Layout.AnnotatedSection
title="..."
description="..."
>
<Card sectioned>
<Form onSubmit={this.handleSubmitEmails}>
<FormLayout>
<TextField
value={emails}
onChange={this.handleChange('emails')}
label="..."
type="emails"
maxlength="200"
/>
<Stack distribution="trailing">
<Button primary submit loading={loadingEmailResponse}>
Save
</Button>
</Stack>
</FormLayout>
</Form>
</Card>
</Layout.AnnotatedSection>
<Layout.AnnotatedSection
title="..."
description="..."
>
<Card sectioned>
<DisplayText size="small"> {domain} </DisplayText>
<br/>
<Form onSubmit={this.handleSubmitDomain}>
<FormLayout>
<TextField
value={alias}
onChange={this.handleChange('alias')}
label="..."
type="text"
maxlength="50"
/>
<Stack distribution="trailing">
<Button primary submit loading={loadingDomainResponse}>
Save
</Button>
</Stack>
</FormLayout>
</Form>
</Card>
</Layout.AnnotatedSection>
</Layout>
</Page>
</Frame>
);
}
handleToast = () => {
this.setState({
err: false,
message: false,
active: false
})
};
handleSubmitEmails = () => {
this.setState({loadingEmailResponse:true});
fetch(`/emails`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
emails: this.state.emails
})
}).then(response => response.json())
.then(result => {
console.log("JSON: "+JSON.stringify(result));
if (result.err) {
this.setState({
err: result.err,
message: result.err,
active: true
})
}
else {
this.setState({message: "...", active: true});
}
this.setState({loadingEmailResponse:false});
});
};
handleSubmitDomain = () => {
this.setState({loadingDomainResponse:true});
fetch(`/domain`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body:
JSON.stringify({
alias: this.state.alias
})
}).then(response => response.json())
.then(result => {
console.log("JSON: "+JSON.stringify(result));
if (result.err) {
this.setState({
err: result.err,
message: result.err,
active: true
})
}
else {
this.setState({message: "...", active: true});
}
this.setState({loadingDomainResponse:false});
});
};
handleChange = (field) => {
return (value) => this.setState({ [field]: value });
};
}
export default Index;
server.js
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
app.prepare().then(() => {
const server = new Koa();
const router = new Router();
server.use(bodyParser());
server.use(session({ secure: true, sameSite: 'none' }, server));
server.keys = [SHOPIFY_API_SECRET_KEY];
router.get('/state', async (ctx) => {
let domain = ctx.session.shop;
let alias;
const snap = await global.db.collection("...").doc(ctx.session.shop).get();
if (snap.data().alias) {
alias = snap.data().alias;
}
let emails = snap.data().emails;
let emailString = "";
if (!emails) {
ctx.response.body = {err: "..."};
}
else if(emails.length < 4) {
for (email of emails) {
emailString += (","+email);
}
theEmailString = emailString.substring(1);
let response = {
domain: domain,
alias: alias,
emails: theEmailString
}
ctx.response.body = response;
}
else {
ctx.response.body = {err: "..."};
}
});
});
Edit
I have provided an initial answer, but I am looking for a better one if possible.
Also, it seems possible to make the Shopify app bridge navigation links use the next.js router instead of triggering full page reloads:
https://shopify.dev/tools/app-bridge/actions/navigation
If someone shares how to do that for next.js with sufficient detail, that would be better than my answer.
I shared load times measured using npm run dev in my question but here is some information about load times in prod mode as well.
Running the app in prod mode (npm run build and npm run start) after removing the embedding into the Shopify admin UI shows that the app takes a total of about 2 seconds to load in prod mode which still seems very slow (Shopify UI was adding about 3 seconds).
The Shopify app bride navigation links would do full page reloads when clicked instead of changing pages like Next.js links would.
Replaced the App Navigation links with Next links.
Still, 1.86 seconds for the first load is very slow, and am open to better solutions.
Your initial load on index, according your dev tools waterfall, took almost 2 seconds for only 18.5KB of data. This is alarmingly slow and prior to the rest of your resources being reached even. My first thought would be network/server lag. Are you hosting this locally or on a web server of some sort?
I would strip it down as much as you can, maybe even just try and load a simple index.html file with only a header. If that takes a couple of seconds to load then you may need to either upgrade or migrate to a better host. If you are hosting locally this could just be an issue of your internet having a low upload speed. Many internet plans have fast downloads but slow uploads and you are not always going to get what your ISP promises.
try optimizing your code by removing any unnecessary code. Try to use more dynamic import such that you get a fast initial bioler plate load up and the heavy codes like charts, graphs and pictures and video load at a later time at the client end.
import dynamic from "next/dynamic" , this should give the client a quick first paint view like youtube does.
https://nextjs.org/docs/advanced-features/dynamic-import
try out react Formik( An optimize form controler for small apps) and also try using function components over Class Components. Using Next you can do most of you database calls in getStatiProps, getServerSideProps, getStaticPaths. for periodic cached fetching use SWR hook.
There is an async function (send) in which if the msg attribute is not defined, another function (getNextProcess) sets its value.
getNextProcess performs a database query within a Promise. The query callback the Promise resolve function.
Problem:
After translating the code with Babel, msg will always be 'undefined'.
Promise runs in the background independently and returns with the correct value (checked with console log).
/* ORIGINAL */
class MyClass(){
/*
construct & other codes here...
*/
getNextProcess(){
new Promise(res => {
logDb.get("SELECT * FROM process WHERE details NOT LIKE '%isComplete:true%' ORDER BY id LIMIT 1", (e, r) => {
let result = JSON.parse(r.details);
result.id = r.id;
res(result);
});
});
}
async send(msg = undefined){
if (!msg) msg = await this.getNextProcess();
webView.send('webview:receive', msg);
}
}
/* Transformed */
{
key: "send",
value: function () {
var _send = (0, _asyncToGenerator2["default"])(
/*#__PURE__*/
_regenerator["default"].mark(function _callee() {
var msg,
_args = arguments;
return _regenerator["default"].wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
msg = _args.length > 0 && _args[0] !== undefined ? _args[0] : undefined;
if (msg) {
_context.next = 5;
break;
}
_context.next = 4;
return this.getNextProcess();
case 4:
msg = _context.sent;
case 5:
this.webView.send('webview:receive', msg);
case 6:
case "end":
return _context.stop();
}
}
}, _callee, this);
}));
function send() {
return _send.apply(this, arguments);
}
return send;
}()
}
/* package.json */
// ...
"scripts": {
"start": "npm run compile && electron main.js",
"compile": "browserify -t [ babelify --presets [ #babel/preset-env ] ] src/app.js -o js/app.js",
},
"devDependencies": {
"#babel/core": "^7.4.5",
"#babel/plugin-transform-async-to-generator": "^7.4.4",
"#babel/plugin-transform-regenerator": "^7.4.5",
"#babel/plugin-transform-runtime": "^7.4.4",
"#babel/preset-env": "^7.4.5",
"#babel/runtime": "^7.4.5",
"babel-core": "^7.0.0-bridge.0",
"babelify": "^10.0.0",
"browserify": "^13.0.1",
"electron": "^4.1.4",
"electron-rebuild": "^1.8.5"
},
// ...
/* .babelrc */
{
"presets": [
[
"#babel/preset-env",
{
"useBuiltIns": "entry"
}
]
],
"ignore": [
"../node_modules/jquery/dist/jquery.js"
],
"plugins": [
["#babel/plugin-transform-runtime", {
"helpers": true,
"regenerator": true
}],
"#babel/plugin-transform-async-to-generator",
"#babel/plugin-transform-regenerator",
]
}
It looks like you forgot to return the promise in getNextProcess.
await doesn't know what to wait for if you don't give it a promise.
getNextProcess(){
return new Promise(res => {
//...
});
}
I am using babeljs to transform my javascript code. I am using async-await to handle asynchronous ajax calls in jquery. I haven't been able to figure out from the Babeljs Documentation exactly what configurations I need in order to get this to work. Using the below configurations I am gettin the error
Module name "babel-polyfill" has not been loaded yet for context: _. Use require([])
What configuration changes (or code changes) do I need to make in order to correctly configure Babel? Any additional explanation of working with Babel/es2015+ would be appreciated.
.babelrc
{
"plugins": [ "transform-async-to-generator" ],
"presets": ["env"]
}
package.json
{
"dependencies": {
"#types/jquery": "^2.0.46",
"#types/papaparse": "^4.1.28",
"#types/xrm": "^8.2.5",
"babel-polyfill": "^6.23.0",
"bootstrap": "^3.3.7",
"papaparse": "^4.3.3",
"requirejs": "^2.3.3",
"vue": "^2.3.3"
},
"devDependencies": {
"babel-cli": "^6.24.1",
"babel-plugin-transform-async-to-generator": "^6.24.1",
"babel-preset-env": "^1.5.2"
},
"name": "mypackage",
"private": true,
"scripts": {
"build": "babel WebResources -d build/CCSEQ"
},
"version": "1.0.0"
}
main.js (pre-babel)
/// <reference path="../node_modules/#types/jquery/index.d.ts" />
"use strict";
require("babel-polyfill");
requirejs.config({
paths: {
PapaParse: "/papaparse.min",
Constants: "/build/CCSEQ/Constants",
Model: "/build/CCSEQ/Model",
WebAPI: "/build/CCSEQ/WebAPI",
Xrm: "/build/CCSEQ/Xrm",
Vue: "/node_modules/vue/dist/vue"
}
})
require(["PapaParse", "Constants", "Model", "WebAPI", "Xrm", "Vue"], function (Papa, Constants, Model, WebAPI, Xrm, Vue) {
function ImportExpenseTransaction(data) {
let newExpenseTransactions = new Array();
let entityID = Xrm.Page.getCurrentEntityID();
data.forEach(async (expense) => {
if (expense[0] !== "PR EMP ID") {
let newExpenseTransaction = new Model.Entity();
newExpenseTransaction.Type = Constants.EntityType.ExpenseTransaction;
newExpenseTransaction.Attributes.push(new Model.EntityField("ccseq_employeeid",
await WebAPI.Get(new Model.QueryDetails(Constants.EntityType.SystemUser, expense[0], ["systemuserid"], [new Model.Condition("ccseq_chnnavid", expense[0], Constants.Condition.EQUALS)])),
Constants.EntityType.SystemUser));
newExpenseTransactions.push(newExpenseTransaction);
}
});
});
main.js (post-babel)
/// <reference path="../node_modules/#types/jquery/index.d.ts" />
"use strict";
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
require("babel-polyfill");
requirejs.config({
paths: {
PapaParse: "/node_modules/papaparse/papaparse.min",
Constants: "/build/CCSEQ/Constants",
Model: "/build/CCSEQ/Model",
WebAPI: "/build/CCSEQ/WebAPI",
Xrm: "/build/CCSEQ/Xrm",
Vue: "/node_modules/vue/dist/vue"
}
});
require(["PapaParse", "Constants", "Model", "WebAPI", "Xrm", "Vue"], function (Papa, Constants, Model, WebAPI, Xrm, Vue) {
function ImportExpenseTransaction(data) {
var _this = this;
var newExpenseTransactions = new Array();
var entityID = Xrm.Page.getCurrentEntityID();
data.forEach(function () {
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(expense) {
var newExpenseTransaction;
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
if (!(expense[0] !== "PR EMP ID")) {
_context.next = 32;
break;
}
newExpenseTransaction = new Model.Entity();
newExpenseTransaction.Type = Constants.EntityType.ExpenseTransaction;
_context.t0 = newExpenseTransaction.Attributes;
_context.t1 = Model.EntityField;
_context.next = 7;
return WebAPI.Get(new Model.QueryDetails(Constants.EntityType.SystemUser, expense[0], ["systemuserid"], [new Model.Condition("ccseq_chnnavid", expense[0], Constants.Condition.EQUALS)]));
case 7:
_context.t2 = _context.sent;
_context.t3 = Constants.EntityType.SystemUser;
_context.t4 = new _context.t1("ccseq_employeeid", _context.t2, _context.t3);
_context.t0.push.call(_context.t0, _context.t4);
newExpenseTransactions.push(newExpenseTransaction);
case 32:
case "end":
return _context.stop();
}
}
}, _callee, _this);
}));
return function (_x) {
return _ref.apply(this, arguments);
};
}());
});
One thing I can see is that you're not actually doing anything with
require("babel-polyfill")
as in require is returning the module's export for you to use. Simply having line 4 in main.js isn't enough.
That said I'm not sure exactly what you're supposed to do with that module in this context.
I ran into a polyfill issue with our test suite (we use karma) and had to work around it like this (from karma.conf.js)
module.exports = function (config) {
config.set({
// to run in additional browsers:
// 1. install corresponding karma launcher
// http://karma-runner.github.io/0.13/config/browsers.html
// 2. add it to the `browsers` array below.
browsers: ['PhantomJS'],
frameworks: ['mocha', 'sinon-chai'],
reporters: ['spec', 'coverage'],
files: [
'../../node_modules/babel-polyfill/dist/polyfill.js',
'./index.js'
],
preprocessors: {
'./index.js': ['webpack', 'sourcemap']
},
webpack: webpackConfig,
webpackMiddleware: {
noInfo: true
},
coverageReporter: {
dir: './coverage',
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text-summary' }
]
}
})
}
I had to add the relative path to polyfill.js (that lives in the babel-polyfill module under /dist) into the files property. Maybe you need to do something similar?
But you'll probably want to store the return value of require into something like
var polyfill = require("babel-polyfill")
and then reference that somewhere.
Hope this helps.
¯\_(ツ)_/¯
maybe even in paths add something like:
Polyfill: "/node_modules/babel-polyfill/dist/polyfill.js"