Skip to content
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 .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,4 @@ scripts
!scripts/build*pwa.js
!scripts/init-development-environment.js
!scripts/compile-docker-scripts.js
!scripts/inject-lib-chunks.js
7 changes: 3 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@ RUN npm run postinstall
ARG serviceWorker
RUN node schematics/customization/service-worker ${serviceWorker} || true
COPY templates/webpack/* /workspace/templates/webpack/
COPY tsconfig.server.json server.ts /workspace/
COPY babel.config.js /workspace/
ARG testing=false
ENV TESTING=${testing}
ARG activeThemes=
RUN if [ ! -z "${activeThemes}" ]; then npm pkg set config.active-themes="${activeThemes}"; fi
RUN npm run build:multi client -- --deploy-url=DEPLOY_URL_PLACEHOLDER
COPY tsconfig.server.json server.ts /workspace/
COPY babel.config.js /workspace/
RUN npm run build:multi server
RUN npm run build:multi -- --deploy-url=DEPLOY_URL_PLACEHOLDER
RUN node scripts/compile-docker-scripts
COPY dist/* /workspace/dist/

Expand Down
12 changes: 6 additions & 6 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,21 @@
{
"type": "bundle",
"name": "main",
"baseline": "350kb",
"baseline": "480kb",
"warning": "100kb",
"error": "200kb"
},
{
"type": "bundle",
"name": "vendor",
"baseline": "800kb",
"baseline": "500kb",
"warning": "100kb",
"error": "300kb"
"error": "450kb"
},
{
"type": "initial",
"maximumWarning": "1500kb",
"maximumError": "2mb"
"maximumWarning": "1600kb",
"maximumError": "1800kb"
},
{
"type": "anyComponentStyle",
Expand Down Expand Up @@ -109,7 +109,7 @@
"options": {
"browserTarget": "intershop-pwa:build"
},
"defaultConfiguration": "b2b,development",
"defaultConfiguration": "b2c,development",
"configurations": {
"production": {
"browserTarget": "intershop-pwa:build:production"
Expand Down
143 changes: 103 additions & 40 deletions scripts/build-multi-pwa.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,57 +8,120 @@ const configurations = (
.split(',')
.map((theme, index) => ({ theme, port: 4000 + index }));

const builds = [];

const processArgs = process.argv.slice(2);
const extraArgs = processArgs.filter(a => a !== 'client' && a !== 'server').join(' ');

if (processArgs.includes('client') || !processArgs.includes('server'))
builds.push(
...configurations.map(({ theme }) =>
`build client --configuration=${theme},production -- --output-path=dist/${theme}/browser --progress=false ${extraArgs}`.trim()
)
);
const clientBuilds = processArgs.includes('client') || !processArgs.includes('server');
const serverBuilds = processArgs.includes('server') || !processArgs.includes('client');

/**
* Spawns a child process and returns a promise that resolves when it completes.
*/
function spawnAsync(command, args, options) {
return new Promise((resolve, reject) => {
const child = cp.spawn(command, args, { shell: true, ...options });
child.on('close', code => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Command failed with exit code ${code}: ${command} ${args.join(' ')}`));
}
});
child.on('error', reject);
});
}

/**
* Builds bundles in parallel for all themes.
*/
async function buildParallel(type, buildFn) {
console.log(`\nBuilding ${type} bundles in parallel...`);
const startTime = Date.now();

if (processArgs.includes('server') || !processArgs.includes('client'))
builds.push(
...configurations.map(({ theme }) =>
`build server --configuration=${theme},production -- --output-path=dist/${theme}/server --progress=false ${extraArgs}`.trim()
)
);
await Promise.all(configurations.map(({ theme }) => buildFn(theme)));

const cores = +process.env.PWA_BUILD_MAX_WORKERS || Math.round(require('os').cpus().length / 3) || 1;
const parallel = cores === 1 ? [] : ['--max-parallel', cores, '--parallel'];
if (parallel) {
console.log(`Using ${cores} cores for multi compile.`);
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(`\n✔ All ${type} bundles built in ${duration}s`);
}

const result = cp.spawnSync(
path.join('node_modules', '.bin', 'npm-run-all' + (process.platform === 'win32' ? '.cmd' : '')),
['--silent', ...parallel, ...builds],
{
stdio: 'inherit',
async function main() {
// Build client bundles in parallel
if (clientBuilds) {
await buildParallel('client', theme =>
spawnAsync(
'npm',
[
'run',
'build',
'client',
'--',
`--configuration=${theme},production`,
`--output-path=dist/${theme}/browser`,
'--progress=false',
extraArgs,
].filter(Boolean),
{ stdio: 'inherit' }
)
);

// Inject critical split chunks for each built theme.
configurations.forEach(({ theme }) => {
const distPath = path.join('dist', theme, 'browser');
if (fs.existsSync(distPath)) {
cp.execSync(`node scripts/inject-lib-chunks.js ${theme}`, { stdio: 'inherit' });
}
});
}

// Build server bundles in parallel
if (serverBuilds) {
await buildParallel('server', theme =>
spawnAsync(
'npm',
[
'run',
'ng',
'--',
'run',
`intershop-pwa:server:${theme},production`,
`--output-path=dist/${theme}/server`,
'--progress=false',
extraArgs,
].filter(Boolean),
{ stdio: 'inherit' }
)
);
}
);
if (result.status !== 0) {
process.exit(result.status);
}

fs.writeFileSync(
'src/ssr/server-scripts/ecosystem-ports.json',
JSON.stringify(
configurations.reduce((acc, { theme, port }) => ({ ...acc, [theme]: port }), {}),
undefined,
2
)
);
main()
.then(() => {
fs.writeFileSync(
'src/ssr/server-scripts/ecosystem-ports.json',
JSON.stringify(
configurations.reduce((acc, { theme, port }) => ({ ...acc, [theme]: port }), {}),
undefined,
2
)
);

configurations.forEach(({ theme }) => {
fs.writeFileSync(
`dist/${theme}/run-standalone.js`,
`const path = require('path');
// Only create run-standalone.js if server build was done
if (serverBuilds) {
configurations.forEach(({ theme }) => {
const serverPath = path.join('dist', theme, 'server');
if (fs.existsSync(serverPath)) {
fs.writeFileSync(
`dist/${theme}/run-standalone.js`,
`const path = require('path');
process.env.BROWSER_FOLDER = path.join(__dirname, 'browser');
require('child_process').fork(path.join(__dirname, 'server', 'main'));
`
);
});
);
}
});
}
})
.catch(err => {
console.error(err.message);
process.exit(1);
});
14 changes: 14 additions & 0 deletions scripts/build-pwa.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ if (client) {
stdio: 'inherit',
});
removeServiceWorkerCacheCheck(remainingArgs);

// Inject critical split chunks into index.html.
const outputPathArg = remainingArgs.find(arg => arg.startsWith('--output-path'));
let themeParam = '';
if (outputPathArg) {
const outputPath = outputPathArg.split('=')[1];
const match = outputPath.match(/^dist\/([^/]+)\/browser$/);
if (match) {
themeParam = match[1];
}
}
execSync(`node scripts/inject-lib-chunks.js ${themeParam}`, {
stdio: 'inherit',
});
}

if (configuration) {
Expand Down
53 changes: 53 additions & 0 deletions scripts/inject-lib-chunks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const fs = require('fs');
const path = require('path');

const theme = process.argv[2];
const distPath = theme
? path.join(__dirname, '..', 'dist', theme, 'browser')
: path.join(__dirname, '..', 'dist', 'browser');
const indexPath = path.join(distPath, 'index.html');

const criticalLibChunks = [
'framework', // Framework split chunk from custom splitChunks config
'ng-core', // Required for Angular bootstrap
'ng-common', // Required for common directives and pipes
'rxjs', // Required for observables throughout the app
];

const libChunks = criticalLibChunks;

console.log(`inject-lib-chunks: Processing ${theme ? theme + '/browser/' : ''}index.html`);

if (!fs.existsSync(indexPath)) {
console.error(`inject-lib-chunks: index.html not found at ${indexPath}`);
process.exit(1);
}

let html = fs.readFileSync(indexPath, 'utf-8');

const distFiles = fs.readdirSync(distPath);
const libFiles = distFiles.filter(f => libChunks.some(lib => f.startsWith(lib)) && f.endsWith('.js'));

const chunksToInject = libFiles.filter(file => !html.includes(file));

if (chunksToInject.length === 0) {
console.log('inject-lib-chunks: no chunks to inject (already present)');
process.exit(0);
}

const mainScriptMatch = html.match(/<script[^>]*src="[^"]*main[^"]*\.js"[^>]*><\/script>/i);
const mainScriptTag = mainScriptMatch?.[0];

const injectHtml = chunksToInject.map(file => `<script src="${file}" type="module" defer></script>`).join('');

if (mainScriptTag) {
html = html.replace(mainScriptTag, injectHtml + mainScriptTag);
} else if (html.includes('</body>')) {
html = html.replace('</body>', injectHtml + '</body>');
} else {
console.error('inject-lib-chunks: could not find main script tag or </body>');
process.exit(1);
}

fs.writeFileSync(indexPath, html, 'utf-8');
console.log(`inject-lib-chunks: injected ${chunksToInject.length} lib chunks`);
27 changes: 27 additions & 0 deletions templates/webpack/webpack.custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,33 @@ export default (config: Configuration, angularJsonConfig: CustomWebpackBrowserSc
return 'common';
},
};

// Keep framework code in one stable chunk to reduce initial script overhead.
cacheGroups.framework = {
test: /[\\/]node_modules[\\/](?:@angular|rxjs)[\\/]/,
chunks: 'all',
name: 'framework',
priority: 60,
enforce: true,
};

// Auth/OAuth code should only be pulled in when auth-related flows are used.
cacheGroups['lib-oauth'] = {
test: /[\\/]node_modules[\\/]angular-oauth2-oidc[\\/]/,
chunks: 'async',
name: 'lib-oauth',
priority: 40,
reuseExistingChunk: true,
};

// Group common UI libs into one lazy chunk.
cacheGroups['lib-ui'] = {
test: /[\\/]node_modules[\\/](?:bootstrap|@ng-bootstrap|@fortawesome|swiper)[\\/]/,
chunks: 'async',
name: 'lib-ui',
priority: 35,
reuseExistingChunk: true,
};
}

if (!process.env.TESTING) {
Expand Down