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

Examples build fixes #6102

Merged
merged 10 commits into from
Mar 5, 2024
2 changes: 1 addition & 1 deletion examples/jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
"esModuleInterop" : true,
"moduleResolution" : "node"
},
"include": ["src", "scripts", "iframe", "examples.config.mjs"],
"include": ["src", "scripts", "iframe", "utils.mjs"],
"exclude": ["node_modules", "src/lib"]
}
4 changes: 2 additions & 2 deletions examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
"main": "index.js",
"type": "module",
"scripts": {
"prebuild": "npm run -s build:metadata && npm run -s build:standalone && npm run -s build:sharing",
"build": "cross-env NODE_ENV=production rollup -c",
"build:pre": "npm run -s build:metadata && npm run -s build:standalone && npm run -s build:sharing",
"build:metadata": "node ./scripts/metadata.mjs",
"build:sharing": "node ./scripts/sharing-html.mjs",
"build:standalone": "node ./scripts/standalone-html.mjs",
"build:thumbnails": "node ./scripts/thumbnails.mjs",
"rollup:watch": "cross-env NODE_ENV=development rollup -c -w",
"serve": "serve dist -l 5000 --no-request-logging --config ../serve.json",
"lint": "eslint . --ext .js,.ts,.tsx",
"watch": "npm run prebuild && cross-env concurrently --kill-others \"npm run rollup:watch\"",
"watch": "cross-env concurrently --kill-others \"npm run rollup:watch\"",
"watch:debug": "cross-env ENGINE_PATH=../build/playcanvas.dbg.js npm run watch",
"watch:profiler": "cross-env ENGINE_PATH=../build/playcanvas.prf.js npm run watch",
"develop": "cross-env NODE_ENV=development concurrently --kill-others \"npm run watch\" \"npm run serve\"",
Expand Down
169 changes: 74 additions & 95 deletions examples/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as fs from 'node:fs';
import fse from 'fs-extra';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { exec } from 'node:child_process';
import { execSync } from 'node:child_process';

// 1st party Rollup plugins
import alias from '@rollup/plugin-alias';
Expand All @@ -12,19 +12,29 @@ import replace from '@rollup/plugin-replace';
import resolve from "@rollup/plugin-node-resolve";
import terser from '@rollup/plugin-terser';

// engine rollup utils
import { buildTarget } from '../utils/rollup-build-target.mjs';
import { scriptTargetEs6 } from '../utils/rollup-script-target-es6.mjs';

// util functions
import { isModuleWithExternalDependencies } from './utils.mjs';

/** @typedef {import('rollup').RollupOptions} RollupOptions */
/** @typedef {import('rollup').Plugin} RollupPlugin */

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const NODE_ENV = process.env.NODE_ENV ?? '';
const ENGINE_PATH = !process.env.ENGINE_PATH && NODE_ENV === 'development' ?
'../src/index.js' :
process.env.ENGINE_PATH ?? '';

const PCUI_PATH = process.env.PCUI_PATH || 'node_modules/@playcanvas/pcui';
const PCUI_REACT_PATH = path.resolve(PCUI_PATH, 'react');
const PCUI_STYLES_PATH = path.resolve(PCUI_PATH, 'styles');

const staticFiles = [

const STATIC_FILES = [
// static main page src
{ src: './src/static', dest: 'dist/' },

Expand All @@ -51,59 +61,36 @@ const staticFiles = [

// modules (N.B. destination folder is 'modules' as 'node_modules' are automatically excluded by git pages)
{ src: './node_modules/monaco-editor/min/vs', dest: 'dist/modules/monaco-editor/min/vs' },
// N.B. fflate will not be needed once extras module is rolled up
{ src: '../node_modules/fflate/esm/', dest: 'dist/modules/fflate/esm' }
];

const regexpExportStarFrom = /^\s*export\s*\*\s*from\s*.+\s*;\s*$/gm;
const regexpExportFrom = /^\s*export\s*{.*}\s*from\s*.+\s*;\s*$/gm;
const regexpImport = /^\s*import\s*.+\s*;\s*$/gm;
/**
* If one of this RegExp's match, it's likely an ESM with external dependencies.
* @example
* isModuleWithExternalDependencies(`
* // Testing variants:
* export * from './index.mjs';
* export { Ray } from './core/shape/ray.js';
* import './polyfill/OESVertexArrayObject.js';
*`);
* @param {string} content - The file content to test.
* @returns {boolean} Whether content is a module.
*/
function isModuleWithExternalDependencies(content) {
const a = regexpExportStarFrom.test(content);
const b = regexpExportFrom.test(content);
const c = regexpImport.test(content);
// console.log('isModuleWithExternalDependencies', { a, b, c });
return a || b || c;
}
// TODO: fflate will not be needed once extras module is rolled up
{ src: '../node_modules/fflate/esm/', dest: 'dist/modules/fflate/esm' },

const { NODE_ENV = '' } = process.env;
let { ENGINE_PATH = '' } = process.env;
// engine path
...getEnginePathFiles()
];

// If we don't set ENGINE_PATH and NODE_ENV is 'development', we use ../src/index.js, which
// requires no additional build shells.
if (!ENGINE_PATH && NODE_ENV === 'development') {
ENGINE_PATH = '../src/index.js';
}
function getEnginePathFiles() {
if (!ENGINE_PATH) {
return [];
}

if (ENGINE_PATH) {
const src = path.resolve(ENGINE_PATH);
const content = fs.readFileSync(src, 'utf8');
const copyDir = isModuleWithExternalDependencies(content);
if (copyDir) {
// Copy entire folder for MJS versions with external dependencies
const isUnpacked = isModuleWithExternalDependencies(content);
if (isUnpacked) {
const srcDir = path.dirname(src);
const dest = 'dist/iframe/ENGINE_PATH';
staticFiles.push({ src: srcDir, dest });
} else {
// This can be both UMD/ESM as a single file
const entryPoint = ENGINE_PATH.split("/").pop();
const dest = 'dist/iframe/ENGINE_PATH/' + entryPoint;
staticFiles.push({ src, dest });
return [{ src: srcDir, dest }];
}

// packed module builds
const dest = 'dist/iframe/ENGINE_PATH/index.js';
return [{ src, dest }];
}

/**
* @returns {RollupPlugin} The plugin.
*/
function timestamp() {
return {
name: 'timestamp',
Expand All @@ -114,7 +101,7 @@ function timestamp() {
}

/**
* @param {import('rollup').Plugin} plugin - The Rollup plugin.
* @param {RollupPlugin} plugin - The Rollup plugin.
* @param {string} src - File or path to watch.
*/
function watch(plugin, src) {
Expand All @@ -138,7 +125,7 @@ function watch(plugin, src) {
/**
* This plugin copies static files from source to destination.
*
* @param {staticFiles} targets - Array of source and destination objects.
* @param {STATIC_FILES} targets - Array of source and destination objects.
* @returns {RollupPlugin} The plugin.
*/
function copyStaticFiles(targets) {
Expand Down Expand Up @@ -181,21 +168,45 @@ function buildAndWatchStandaloneExamples() {
}
},
generateBundle() {
const cmd = `cross-env NODE_ENV=${NODE_ENV} ENGINE_PATH=${ENGINE_PATH} npm run build:standalone`;
const cmd = `cross-env NODE_ENV=${NODE_ENV} ENGINE_PATH=${ENGINE_PATH} npm run build:pre`;
console.log(cmd);
exec(cmd);
execSync(cmd, { stdio: 'inherit' });
}
};
}

// define supported module overrides
const aliasEntries = {
'@playcanvas/pcui/react': PCUI_REACT_PATH,
'@playcanvas/pcui/styles': PCUI_STYLES_PATH
};
function getEngineTargets() {
const targets = [
// Outputs: dist/iframe/playcanvas-extras.mjs
scriptTargetEs6('pcx', '../extras/index.js', 'dist/iframe/playcanvas-extras.mjs')
];
if (NODE_ENV === 'production') {
// Outputs: dist/iframe/playcanvas.mjs
targets.push(buildTarget('release', 'es6', '../src/index.js', 'dist/iframe'));
}
if (NODE_ENV === 'production' || NODE_ENV === 'development') {
// Outputs: dist/iframe/playcanvas.dbg.mjs
targets.push(buildTarget('debug', 'es6', '../src/index.js', 'dist/iframe'));
}
if (NODE_ENV === 'production' || NODE_ENV === 'profiler') {
// Outputs: dist/iframe/playcanvas.prf.mjs
targets.push(buildTarget('profiler', 'es6', '../src/index.js', 'dist/iframe'));
}
return targets;
}

/** @type {RollupOptions[]} */
const targets = [
export default [
{
input: 'src/static/index.html',
output: {
file: `dist/copy.tmp`
},
plugins: [
buildAndWatchStandaloneExamples(),
copyStaticFiles(STATIC_FILES),
timestamp()
]
},
{
// A debug build is ~2.3MB and a release build ~0.6MB
input: 'src/app/index.mjs',
Expand All @@ -204,7 +215,13 @@ const targets = [
format: 'umd'
},
plugins: [
alias({ entries: aliasEntries }),
alias({
entries: {
// define supported module overrides
'@playcanvas/pcui/react': PCUI_REACT_PATH,
'@playcanvas/pcui/styles': PCUI_STYLES_PATH
}
}),
commonjs(),
resolve(),
replace({
Expand All @@ -217,43 +234,5 @@ const targets = [
timestamp()
]
},
{
input: 'src/static/index.html',
output: {
file: `dist/copy.tmp`
},
plugins: [
copyStaticFiles(staticFiles),
buildAndWatchStandaloneExamples(),
timestamp()
]
},
scriptTargetEs6('pcx', '../extras/index.js', 'dist/iframe/playcanvas-extras.mjs')
...getEngineTargets()
];

// We skip building PlayCanvas ourselves when ENGINE_PATH is given.
// In that case we have a watcher which copies all necessary files.
if (ENGINE_PATH === '') {
/** @type {buildTarget} */
const pushTarget = (...args) => {
targets.push(buildTarget(...args));
};
if (NODE_ENV === 'production') {
// Outputs: dist/iframe/playcanvas.mjs
pushTarget('release', 'es6', '../src/index.js', 'dist/iframe');
// Outputs: dist/iframe/playcanvas.dbg.mjs
pushTarget('debug', 'es6', '../src/index.js', 'dist/iframe');
// Outputs: dist/iframe/playcanvas.prf.mjs
pushTarget('profiler', 'es6', '../src/index.js', 'dist/iframe');
} else if (NODE_ENV === 'development') {
// Outputs: dist/iframe/playcanvas.dbg.mjs
pushTarget('debug', 'es6', '../src/index.js', 'dist/iframe');
} else if (NODE_ENV === 'profiler') {
// Outputs: dist/iframe/playcanvas.prf.mjs
pushTarget('profiler', 'es6', '../src/index.js', 'dist/iframe');
} else {
console.warn("NODE_ENV is neither production, development nor profiler.");
}
}

export default targets;
6 changes: 3 additions & 3 deletions examples/scripts/standalone-html.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ function engineFor(type) {
case 'DEVELOPMENT':
return './ENGINE_PATH/index.js';
case 'PERFORMANCE':
return './playcanvas.prf.mjs/index.js';
return './playcanvas.prf.mjs';
case 'DEBUG':
return './playcanvas.dbg.mjs/index.js';
return './playcanvas.dbg.mjs';
}
return './playcanvas.mjs/index.js';
return './playcanvas.mjs';
}

/**
Expand Down
21 changes: 21 additions & 0 deletions examples/utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const regexPatterns = [
/^\s*export\s*\*\s*from\s*.+\s*;\s*$/gm,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to worry about dynamic imports? Also should we allow statements without semi-colons? I guess we enforce this on the linting level?

/^\s*export\s*{.*}\s*from\s*.+\s*;\s*$/gm,
/^\s*import\s*.+\s*;\s*$/gm,
];

/**
* Checks if the provided content matches any of a set of patterns indicative of an ES Module with external dependencies.
* Patterns checked include certain export and import statement formats.
*
* @param {string} content The file content to test.
* @returns {boolean} Whether the content is likely an ES Module with external dependencies.
* @example
* isModuleWithExternalDependencies(`
* // Testing variants:
* export * from './index.mjs';
* export { Ray } from './core/shape/ray.js';
* import './polyfill/OESVertexArrayObject.js';
* `);
*/
export const isModuleWithExternalDependencies = (content) => regexPatterns.some(pattern => pattern.test(content));