Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for ES Modules in p5.js-web-editor #3358

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const RENAME_PROJECT = 'RENAME_PROJECT';
export const PROJECT_SAVE_SUCCESS = 'PROJECT_SAVE_SUCCESS';
export const PROJECT_SAVE_FAIL = 'PROJECT_SAVE_FAIL';
export const NEW_PROJECT = 'NEW_PROJECT';
export const NEW_MODULE_PROJECT = 'NEW_MODULE_PROJECT';
export const RESET_PROJECT = 'RESET_PROJECT';

export const SET_PROJECT = 'SET_PROJECT';
Expand Down
7 changes: 7 additions & 0 deletions client/modules/IDE/actions/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,13 @@ export function newProject() {
return resetProject();
}

export function newModuleProject() {
browserHistory.push('/', { confirmed: true, moduleProject: true });
return {
type: ActionTypes.NEW_MODULE_PROJECT
};
}

function generateNewIdsForChildren(file, files) {
const newChildren = [];
file.children.forEach((childId) => {
Expand Down
4 changes: 4 additions & 0 deletions client/modules/IDE/components/Header/Nav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ const ProjectMenu = () => {
const { t } = useTranslation();
const {
newSketch,
newModuleSketch,
saveSketch,
downloadSketch,
shareSketch
Expand Down Expand Up @@ -165,6 +166,9 @@ const ProjectMenu = () => {
</li>
<MenubarSubmenu id="file" title={t('Nav.File.Title')}>
<MenubarItem onClick={newSketch}>{t('Nav.File.New')}</MenubarItem>
<MenubarItem onClick={newModuleSketch}>
{t('Nav.File.NewModule')}
</MenubarItem>
<MenubarItem
hideIf={
!getConfig('LOGIN_ENABLED') || (project?.owner && !isUserOwner)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,15 @@ exports[`Nav renders editor version for desktop 1`] = `
New
</button>
</li>
<li
class="nav__dropdown-item"
>
<button
role="menuitem"
>
New Module
</button>
</li>
</ul>
</li>
<li
Expand Down
12 changes: 12 additions & 0 deletions client/modules/IDE/hooks/useSketchActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
autosaveProject,
exportProjectAsZip,
newProject,
newModuleProject,
saveProject,
setProjectName
} from '../actions/project';
Expand Down Expand Up @@ -32,6 +33,16 @@ const useSketchActions = () => {
}
}

function newModuleSketch() {
if (!unsavedChanges) {
dispatch(showToast('Toast.OpenedNewModuleSketch'));
dispatch(newModuleProject());
} else if (window.confirm(t('Nav.WarningUnsavedChanges'))) {
dispatch(showToast('Toast.OpenedNewModuleSketch'));
dispatch(newModuleProject());
}
}

function saveSketch(cmController) {
if (authenticated) {
dispatch(saveProject(cmController?.getContent()));
Expand Down Expand Up @@ -62,6 +73,7 @@ const useSketchActions = () => {

return {
newSketch,
newModuleSketch,
saveSketch,
downloadSketch,
shareSketch,
Expand Down
65 changes: 53 additions & 12 deletions client/modules/IDE/reducers/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import * as ActionTypes from '../../../constants';
import {
defaultSketch,
defaultCSS,
defaultHTML
defaultHTML,
defaultModuleSketch,
defaultModuleHTML,
createDefaultModuleFiles
} from '../../../../server/domain-objects/createDefaultFiles';

export const initialState = () => {
Expand Down Expand Up @@ -51,6 +54,51 @@ export const initialState = () => {
];
};

export const moduleState = () => {
const a = objectID().toHexString();
const b = objectID().toHexString();
const c = objectID().toHexString();
const r = objectID().toHexString();
return [
{
name: 'root',
id: r,
_id: r,
children: [b, a, c],
fileType: 'folder',
content: ''
},
{
name: 'sketch.js',
content: defaultModuleSketch,
id: a,
_id: a,
isSelectedFile: true,
fileType: 'file',
children: [],
filePath: ''
},
{
name: 'index.html',
content: defaultModuleHTML,
id: b,
_id: b,
fileType: 'file',
children: [],
filePath: ''
},
{
name: 'style.css',
content: defaultCSS,
id: c,
_id: c,
fileType: 'file',
children: [],
filePath: ''
}
];
};

function getAllDescendantIds(state, nodeId) {
return state
.find((file) => file.id === nodeId)
Expand Down Expand Up @@ -158,17 +206,10 @@ const files = (state, action) => {
}
return Object.assign({}, file, { blobURL: action.blobURL });
});
case ActionTypes.NEW_PROJECT: {
const newFiles = action.files.map((file) => {
const corrospondingObj = state.find((obj) => obj.id === file.id);
if (corrospondingObj && corrospondingObj.fileType === 'folder') {
const isFolderClosed = corrospondingObj.isFolderClosed || false;
return { ...file, isFolderClosed };
}
return file;
});
return setFilePaths(newFiles);
}
case ActionTypes.NEW_PROJECT:
return initialState();
case ActionTypes.NEW_MODULE_PROJECT:
return moduleState();
case ActionTypes.SET_PROJECT: {
const newFiles = action.files.map((file) => {
const corrospondingObj = state.find((obj) => obj.id === file.id);
Expand Down
91 changes: 77 additions & 14 deletions client/modules/Preview/EmbedFrame.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,22 @@ function resolveCSSLinksInString(content, files) {
return newContent;
}

function jsPreprocess(jsText) {
function jsPreprocess(jsText, isModule = false) {
let newContent = jsText;
// check the code for js errors before sending it to strip comments
// or loops.
// If this is a module, we need to be careful with transformations
// as they might break import/export statements
if (isModule || /\b(import|export)\b/.test(jsText)) {
// For modules, we still want to check for errors but we'll be more careful with transformations
JSHINT(newContent, { esversion: 11, module: true });
// For modules, we only decomment but don't apply loop protection
// as it might break module semantics
newContent = decomment(newContent, {
ignore: /\/\/\s*noprotect/g,
space: true
});
return newContent;
}
// For regular scripts, apply the standard processing
JSHINT(newContent);

if (JSHINT.errors.length === 0) {
Expand All @@ -87,6 +99,9 @@ function jsPreprocess(jsText) {

function resolveJSLinksInString(content, files) {
let newContent = content;
// Check if this is an ES module (contains import/export statements)
const isModule = /\b(import|export)\b/.test(content);
// Handle regular string references to files
let jsFileStrings = content.match(STRING_REGEX);
jsFileStrings = jsFileStrings || [];
jsFileStrings.forEach((jsFileString) => {
Expand All @@ -101,23 +116,63 @@ function resolveJSLinksInString(content, files) {
jsFileString,
quoteCharacter + resolvedFile.url + quoteCharacter
);
} else if (resolvedFile.name.match(PLAINTEXT_FILE_REGEX)) {
newContent = newContent.replace(
jsFileString,
quoteCharacter + resolvedFile.blobUrl + quoteCharacter
);
}
}
}
});

return jsPreprocess(newContent);
// If this is a module, also handle import statements
if (isModule) {
// Match import statements like: import { x } from './file.js';
// or import x from './file.js';
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?(['"])([^'"]+)(['"])/g;
let importMatch;
// eslint-disable-next-line no-cond-assign
while ((importMatch = importRegex.exec(content))) {
const [fullMatch, openQuote, importPath, closeQuote] = importMatch;
// Only process relative imports, not package imports
if (importPath.startsWith('./') || importPath.startsWith('../')) {
const resolvedFile = resolvePathToFile(importPath, files);
if (resolvedFile) {
if (resolvedFile.url) {
// Replace the import path with the resolved URL
newContent = newContent.replace(
fullMatch,
fullMatch.replace(
`${openQuote}${importPath}${closeQuote}`,
`${openQuote}${resolvedFile.url}${closeQuote}`
)
);
} else {
// Create a blob URL for the imported file
const blobUrl = createBlobUrl(resolvedFile);
const blobPath = blobUrl.split('/').pop();
objectUrls[
blobUrl
] = `${resolvedFile.filePath}/${resolvedFile.name}`;
objectPaths[blobPath] = resolvedFile.name;
// Replace the import path with the blob URL
newContent = newContent.replace(
fullMatch,
fullMatch.replace(
`${openQuote}${importPath}${closeQuote}`,
`${openQuote}${blobUrl}${closeQuote}`
)
);
}
}
}
}
}
// Apply preprocessing with module awareness
return jsPreprocess(newContent, isModule);
}

function resolveScripts(sketchDoc, files) {
const scriptsInHTML = sketchDoc.getElementsByTagName('script');
const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML);
scriptsInHTMLArray.forEach((script) => {
// Check if this is a module script
const isModule = script.getAttribute('type') === 'module';
if (
script.getAttribute('src') &&
script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null
Expand All @@ -137,9 +192,10 @@ function resolveScripts(sketchDoc, files) {
// }${resolvedFile.name}`;
objectUrls[blobUrl] = `${resolvedFile.filePath}/${resolvedFile.name}`;
objectPaths[blobPath] = resolvedFile.name;
// script.setAttribute('data-tag', `${startTag}${resolvedFile.name}`);
// script.removeAttribute('src');
// script.innerHTML = resolvedFile.content; // eslint-disable-line
// Preserve the module type if it was set
if (isModule) {
script.setAttribute('type', 'module');
}
}
}
} else if (
Expand All @@ -149,7 +205,14 @@ function resolveScripts(sketchDoc, files) {
) !== null
) {
script.setAttribute('crossorigin', '');
script.innerHTML = resolveJSLinksInString(script.innerHTML, files); // eslint-disable-line
// For inline scripts, we need to handle module content differently
// to preserve ES module semantics
if (isModule) {
// For module scripts, we don't apply loop protection as it might break imports
script.innerHTML = resolveJSLinksInString(script.innerHTML, files); // eslint-disable-line
} else {
script.innerHTML = resolveJSLinksInString(script.innerHTML, files); // eslint-disable-line
}
}
});
}
Expand Down
40 changes: 40 additions & 0 deletions server/domain-objects/createDefaultFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ function draw() {
background(220);
}`;

export const defaultModuleSketch = `// ES Module version
export function setup() {
createCanvas(400, 400);
}

export function draw() {
background(220);
}`;

export const defaultHTML = `<!DOCTYPE html>
<html lang="en">
<head>
Expand All @@ -23,6 +32,23 @@ export const defaultHTML = `<!DOCTYPE html>
</html>
`;

export const defaultModuleHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/addons/p5.sound.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
<meta charset="utf-8" />

</head>
<body>
<main>
</main>
<script src="sketch.js" type="module"></script>
</body>
</html>
`;

export const defaultCSS = `html, body {
margin: 0;
padding: 0;
Expand All @@ -45,3 +71,17 @@ export default function createDefaultFiles() {
}
};
}

export function createDefaultModuleFiles() {
return {
'index.html': {
content: defaultModuleHTML
},
'style.css': {
content: defaultCSS
},
'sketch.js': {
content: defaultModuleSketch
}
};
}
4 changes: 3 additions & 1 deletion translations/locales/en-US/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"File": {
"Title": "File",
"New": "New",
"NewModule": "New Module",
"Share": "Share",
"Duplicate": "Duplicate",
"Open": "Open",
Expand Down Expand Up @@ -133,6 +134,7 @@
},
"Toast": {
"OpenedNewSketch": "Opened new sketch.",
"OpenedNewModuleSketch": "Opened new module sketch.",
"SketchSaved": "Sketch saved.",
"SketchFailedSave": "Failed to save sketch.",
"AutosaveEnabled": "Autosave enabled.",
Expand Down Expand Up @@ -362,7 +364,7 @@
"CreateTokenSubmit": "Create",
"NoTokens": "You have no existing tokens.",
"NewTokenTitle": "Your new access token",
"NewTokenInfo": "Make sure to copy your new personal access token now.\n You wont be able to see it again!",
"NewTokenInfo": "Make sure to copy your new personal access token now.\n You won't be able to see it again!",
"ExistingTokensTitle": "Existing tokens"
},
"APIKeyList": {
Expand Down