UNPKG

apostrophe

Version:
1,636 lines (1,457 loc) • 56.4 kB
const t = require('../test-lib/test.js'); const assert = require('assert').strict; const fs = require('fs-extra'); const path = require('path'); const Promise = require('bluebird'); const { checkModulesWebpackConfig, formatRebundleConfig, verifyRebundleConfig, getWebpackExtensions, fillExtraBundles } = require('../modules/@apostrophecms/asset/lib/webpack/utils'); const badModules = { badModuleConfig: { options: { ignoreNoCodeWarning: true }, webpack: { badprop: {} } }, badModuleConfig2: { options: { ignoreNoCodeWarning: true }, webpack: [] } }; const pagesToInsert = (homeId) => ([ { _id: 'parent:en:published', aposLocale: 'en:published', metaType: 'doc', aposDocId: 'parent', type: 'bundle-page', slug: '/bundle', visibility: 'public', path: `${homeId.replace(':en:published', '')}/bundle`, level: 1, rank: 0, main: { _id: 'areaId', metaType: 'area', items: [ { _id: 'widgetId', metaType: 'widget', type: 'bundle' } ] } }, { _id: 'child:en:published', title: 'Bundle', aposLocale: 'en:published', metaType: 'doc', aposDocId: 'child', type: 'bundle', slug: 'child', visibility: 'public' } ]); const modules = { '@apostrophecms/page': { options: { park: [], types: [ { name: 'bundle-page', label: 'Bundle Page' } ] } }, bundle: {}, 'bundle-page': {}, 'bundle-widget': {} }; describe('Assets', function() { let apos; const { publicFolderPath, cacheFolderPath, getScriptMarkup, getStylesheetMarkup, expectedBundlesNames, deleteBuiltFolders, allBundlesAreIncluded, removeCache, getCacheMeta, retryAssertTrue } = loadUtils(); after(async function() { await deleteBuiltFolders(publicFolderPath, true); await removeCache(); await t.destroy(apos); }); afterEach(function() { // Prevent hang forever if particular tests fail while testing prod. process.env.NODE_ENV = 'development'; }); this.timeout(5 * 60 * 1000); it('should exist on the apos object', async function() { apos = await t.create({ root: module, modules }); assert(apos.asset); }); it('emits relative entrypoint imports on non-Windows systems', async function() { if (process.platform === 'win32') { return this.skip(); } const componentPath = './modules/example-module/ui/src/index.js'; const absolutePath = path.join( process.cwd(), 'packages/apostrophe/test/modules/example-module/ui/src/index.js' ); const { importCode } = apos.asset.getImportFileOutput([ { component: componentPath, path: absolutePath } ]); assert(importCode.includes(`from "${componentPath}"`)); }); it('should serve static files', async function() { const text = await apos.http.get('/static-test.txt'); assert(text.match(/served/)); }); it('should check that webpack configs in modules are well formatted', async function () { const translate = apos.task.getReq().t; assert.doesNotThrow(() => checkModulesWebpackConfig(apos.modules, translate)); await t.destroy(apos); apos = await t.create({ root: module, modules: badModules }); assert.throws(() => checkModulesWebpackConfig(apos.modules, translate)); await t.destroy(apos); }); it('should get webpack extensions from modules and fill extra bundles', async function () { const expectedEntryPointsNames = { js: [ 'company', 'main', 'another', 'extra', 'extra2' ], css: [ 'company', 'main', 'extra' ] }; apos = await t.create({ root: module, modules: { '@company/bundle': {}, ...modules } }); const { extensions, verifiedBundles } = await getWebpackExtensions({ name: 'src', getMetadata: apos.synth.getMetadata, modulesToInstantiate: apos.modulesToBeInstantiated() }); assert(Object.keys(extensions).length === 2); assert(!extensions.ext1.resolve.alias.ext1); assert(extensions.ext1.resolve.alias.ext1Overriden); assert(extensions.ext2.resolve.alias.ext2); assert.equal( Object.keys(verifiedBundles).length, Math.max(expectedEntryPointsNames.js.length, expectedEntryPointsNames.css.length) ); assert(verifiedBundles.main.js.length, 2); assert(verifiedBundles.main.scss.length, 1); // The local and the npm module source assert.equal(verifiedBundles.company.js.length, 2); assert.equal(verifiedBundles.company.scss.length, 2); assert.equal(verifiedBundles.extra.js.length, 1); assert.equal(verifiedBundles.extra.scss.length, 1); assert.equal(verifiedBundles.extra2.js.length, 1); assert.equal(verifiedBundles.extra2.scss.length, 0); const filled = fillExtraBundles(verifiedBundles); assert.deepEqual(filled.js, expectedEntryPointsNames.js); assert.deepEqual(filled.css, expectedEntryPointsNames.css); }); it('should build the right bundles in dev and prod modes', async function () { process.env.NODE_ENV = 'production'; await apos.asset.tasks.build.task(); const getPath = (p) => `${publicFolderPath}/apos-frontend/` + p; const [ releaseId ] = await fs.readdir(getPath('releases')); const checkFileExists = async (p) => fs.pathExists(getPath(p)); const releasePath = `releases/${releaseId}/default/`; await checkBundlesExists(releasePath, expectedBundlesNames); await deleteBuiltFolders(publicFolderPath); process.env.NODE_ENV = 'development'; await apos.asset.tasks.build.task(); async function checkBundlesExists (folderPath, fileNames) { for (const fileName of fileNames) { const extraBundleExists = await checkFileExists(folderPath + fileName); assert(extraBundleExists); } } return checkBundlesExists('default/', expectedBundlesNames); }); it('should load the right bundles inside the right page', async function () { const { _id: homeId } = await apos.page .find(apos.task.getAnonReq(), { level: 0 }) .toObject(); const jar = apos.http.jar(); await apos.doc.db.insertMany(pagesToInsert(homeId)); const bundlePage = await apos.http.get('/bundle', { jar }); assert(bundlePage.includes(getStylesheetMarkup('public-bundle'))); assert(bundlePage.includes(getStylesheetMarkup('main-bundle'))); assert(!bundlePage.includes(getStylesheetMarkup('extra-bundle'))); assert(bundlePage.includes(getScriptMarkup('public-module-bundle'))); assert(bundlePage.includes(getScriptMarkup('main-module-bundle'))); assert(!bundlePage.includes(getScriptMarkup('extra-module-bundle'))); assert(bundlePage.includes(getScriptMarkup('extra2-module-bundle'))); const childPage = await apos.http.get('/bundle/child', { jar }); assert(childPage.includes(getStylesheetMarkup('public-bundle'))); assert(bundlePage.includes(getStylesheetMarkup('main-bundle'))); assert(childPage.includes(getStylesheetMarkup('extra-bundle'))); assert(childPage.includes(getScriptMarkup('public-module-bundle'))); assert(bundlePage.includes(getScriptMarkup('main-module-bundle'))); assert(childPage.includes(getScriptMarkup('extra-module-bundle'))); assert(!childPage.includes(getScriptMarkup('extra2-module-bundle'))); }); it('should load all the bundles on all pages when the user is logged in', async function () { const user = { ...apos.user.newInstance(), title: 'toto', username: 'toto', password: 'tata', email: 'toto@mail.com', role: 'admin' }; const jar = apos.http.jar(); await apos.user.insert(apos.task.getReq(), user); await apos.http.post( '/api/v1/@apostrophecms/login/login', { method: 'POST', body: { username: 'toto', password: 'tata', session: true }, jar } ); const homePage = await apos.http.get('/', { jar }); assert(homePage.match(/logged in/)); const bundlePage = await apos.http.get('/bundle', { jar }); allBundlesAreIncluded(bundlePage); await t.destroy(apos); }); it('should build with cache and gain performance', async function() { await removeCache(); await removeCache(cacheFolderPath.replace('/webpack-cache', '/changed')); apos = await t.create({ root: module, modules }); assert.throws(() => fs.readdirSync(cacheFolderPath), { code: 'ENOENT' }); let startTime; // Cold run startTime = Date.now(); await apos.asset.tasks.build.task({ 'check-apos-build': false }); const execTime = Date.now() - startTime; const { meta, folders } = getCacheMeta(); assert.equal(folders.length, 2); assert.equal(Object.keys(meta).length, 2); assert(meta['default:apos']); assert(meta['default:src']); // Cache startTime = Date.now(); await apos.asset.tasks.build.task({ 'check-apos-build': false }); const execTimeCached = Date.now() - startTime; const { meta: meta2, folders: folders2 } = getCacheMeta(); assert.equal(folders2.length, 2); assert.equal(Object.keys(meta2).length, 2); assert(meta2['default:apos']); assert(meta2['default:src']); // Expect at least 40% gain, in reallity it should be 50+ const gain = (execTime - execTimeCached) / execTime * 100; assert(gain >= 20, `Expected gain >=20%, got ${gain}%`); // Modification times assert(meta['default:apos'].mdate); assert(meta2['default:apos'].mdate); assert(meta['default:src'].mdate); assert(meta2['default:src'].mdate); assert( new Date(meta['default:apos'].mdate) < new Date(meta2['default:apos'].mdate) ); assert.equal( new Date(meta2['default:apos'].mdate).toISOString(), fs.statSync(meta2['default:apos'].location).mtime.toISOString() ); assert( new Date(meta['default:src'].mdate) < new Date(meta2['default:src'].mdate) ); assert.equal( new Date(meta2['default:src'].mdate).toISOString(), fs.statSync(meta2['default:src'].location).mtime.toISOString() ); }); it('should invalidate build cache when namespace changes', async function() { process.env.APOS_DEBUG_NAMESPACE = 'test'; await apos.asset.tasks.build.task({ 'check-apos-build': false }); const { meta, folders } = getCacheMeta(); assert.equal(folders.length, 4); assert.equal(Object.keys(meta).length, 4); assert(meta['test:apos']); assert(meta['test:src']); assert(meta['default:apos']); assert(meta['default:src']); delete process.env.APOS_DEBUG_NAMESPACE; }); it('should invalidate build cache when packages change', async function() { await t.destroy(apos); const lock = require('./package-lock.json'); assert.equal(lock.version, 'current'); lock.version = 'new'; fs.writeFileSync( path.join(process.cwd(), 'test/package-lock.json'), JSON.stringify(lock, null, ' '), 'utf8' ); apos = await t.create({ root: module, modules }); await apos.asset.tasks.build.task({ 'check-apos-build': false }); const { meta, folders } = getCacheMeta(); assert.equal(folders.length, 6); assert.equal(Object.keys(meta).length, 6); assert(meta['default:apos_2']); assert(meta['default:src_2']); }); it('should invalidate build cache when configuration changes', async function() { await t.destroy(apos); const customModules = { ...modules, 'bundle-page': { webpack: { extensions: { ext1: { resolve: { alias: { ext1: 'changed' } } } } } } }; apos = await t.create({ root: module, modules: customModules }); await apos.asset.tasks.build.task({ 'check-apos-build': false }); const { meta, folders } = getCacheMeta(); assert.equal(folders.length, 7); assert.equal(Object.keys(meta).length, 7); assert(!meta['default:apos_3']); assert(meta['default:src_3']); }); it('should clear build cache', async function() { const cacheFolders = fs.readdirSync(cacheFolderPath, 'utf8'); assert(cacheFolders.length > 0); await apos.asset.tasks['clear-cache'].task(); assert.equal(fs.readdirSync(cacheFolderPath, 'utf8').length, 0); }); it('should be able to override the build cache location via APOS_ASSET_CACHE', async function() { await t.destroy(apos); await removeCache(); const altCacheLoc = cacheFolderPath.replace('/webpack-cache', '/changed'); await removeCache(altCacheLoc); process.env.APOS_ASSET_CACHE = altCacheLoc; apos = await t.create({ root: module, modules }); assert.throws(() => fs.readdirSync(altCacheLoc), { code: 'ENOENT' }); await apos.asset.tasks.build.task({ 'check-apos-build': false }); const { meta, folders } = getCacheMeta(altCacheLoc); assert.equal(folders.length, 2); assert.equal(Object.keys(meta).length, 2); assert(meta['default:apos']); assert(meta['default:src']); delete process.env.APOS_ASSET_CACHE; await removeCache(altCacheLoc); }); it('should watch and rebuild assets and reload page in development (bundle src)', async function() { await t.destroy(apos); let result = {}; const cb = (obj) => { result = obj; }; apos = await t.create({ root: module, autoBuild: true, modules: { ...modules, '@apostrophecms/asset': { extendMethods() { return { async watchUiAndRebuild(_super) { return _super(cb); } }; } } } }); const restartId = apos.asset.restartId; assert(apos.asset.buildWatcher); assert(apos.asset.restartId); assert(!result.builds); assert(!result.changes); // Modify asset and rebuild const assetPath = path.join(process.cwd(), 'test/modules/bundle-page/ui/src/extra.js'); const assetPathPublic = path.join(process.cwd(), 'test/public/apos-frontend/default/extra-module-bundle.js'); const assetContent = fs.readFileSync(assetPath, 'utf-8'); fs.writeFileSync( assetPath, 'export default () => { \'bundle-page-watcher-test-src\'; };\n', 'utf8' ); await retryAssertTrue( async () => (await fs.readFile(assetPathPublic, 'utf8')).match(/bundle-page-watcher-test-src/), 'Unable to verify public asset was rebuilt by the watcher', 500, 10000 ); await retryAssertTrue( () => apos.asset.restartId !== restartId, 'Unable to verify restartId has been changed', 500, 10000 ); await retryAssertTrue( () => result.builds.length === 1 && result.builds.includes('src'), 'Unable to verify build "src" has been triggered', 50, 1000 ); await retryAssertTrue( () => result.changes.length === 1 && result.changes[0].includes('modules/bundle-page/ui/src/extra.js'), 'Unable to verify changes contain the proper file', 50, 1000 ); await t.destroy(apos); assert.equal(apos.asset.buildWatcher, null); apos = null; fs.writeFileSync(assetPath, assetContent, 'utf8'); }); it('should watch and rebuild assets and reload page in development (src)', async function() { await t.destroy(apos); let result = {}; const setTestResult = (obj) => { result = obj; }; const rootPath = process.cwd(); const assetPathJs = path.join(rootPath, 'test/modules/default-page/ui/src/index.js'); const assetPathScss = path.join(rootPath, 'test/modules/default-page/ui/src/index.scss'); const assetPathPublicJs = path.join(rootPath, 'test/public/apos-frontend/default/public-module-bundle.js'); const assetPathPublicCss = path.join(rootPath, 'test/public/apos-frontend/default/public-bundle.css'); const assetPathAposJs = path.join(rootPath, 'test/public/apos-frontend/default/apos-module-bundle.js'); const assetPathAposCss = path.join(rootPath, 'test/public/apos-frontend/default/apos-bundle.css'); const assetContentJs = fs.readFileSync(assetPathJs, 'utf-8'); const assetContentScss = fs.readFileSync(assetPathScss, 'utf-8'); // Resurrect the default assets content if test has failed fs.writeFileSync(assetPathJs, assetContentJs, 'utf8'); fs.writeFileSync(assetPathScss, assetContentScss, 'utf8'); apos = await t.create({ root: module, autoBuild: true, modules: { 'default-page': {}, '@apostrophecms/asset': { extendMethods() { return { async watchUiAndRebuild(_super) { return _super(setTestResult); } }; } } } }); // Assert defaults const restartId = apos.asset.restartId; assert(apos.asset.buildWatcher); assert(apos.asset.restartId); assert(!result.builds); assert(!result.changes); // * modify assets and rebuild fs.writeFileSync( assetPathJs, 'export default () => { \'default-page-watcher-test-src\'; };\n', 'utf8' ); fs.writeFileSync( assetPathScss, '.default-page-watcher-test-src{color:red;}\n', 'utf8' ); // * change is in the public bundle await retryAssertTrue( async () => (await fs.readFile(assetPathPublicJs, 'utf8')).match(/default-page-watcher-test-src/), 'Unable to verify public JS asset was rebuilt by the watcher', 500, 10000 ); await retryAssertTrue( async () => (await fs.readFile(assetPathPublicCss, 'utf8')).match(/\.default-page-watcher-test-src/), 'Unable to verify public CSS asset was rebuilt by the watcher', 500, 10000 ); // * change is in the apos bundle await retryAssertTrue( async () => (await fs.readFile(assetPathAposJs, 'utf8')).match(/default-page-watcher-test-src/), 'Unable to verify apos JS asset was rebuilt by the watcher', 500, 10000 ); await retryAssertTrue( async () => (await fs.readFile(assetPathAposCss, 'utf8')).match(/\.default-page-watcher-test-src/), 'Unable to verify apos CSS asset was rebuilt by the watcher', 500, 10000 ); // * page has been restarted await retryAssertTrue( () => apos.asset.restartId !== restartId, 'Unable to verify restartId has been changed', 500, 10000 ); // * only src related builds were triggered await retryAssertTrue( () => result.builds.length === 1 && result.builds.includes('src'), 'Unable to verify build "src" has been triggered', 50, 1000 ); // * changes detected await retryAssertTrue( () => result.changes.length === 2 && result.changes .filter(f => (f.includes('modules/default-page/ui/src/index.js') || f.includes('modules/default-page/ui/src/index.scss')) ) .length === 2, 'Unable to verify changes contain the proper source files', 50, 1000 ); await t.destroy(apos); assert.equal(apos.asset.buildWatcher, null); apos = null; fs.writeFileSync(assetPathJs, assetContentJs, 'utf8'); fs.writeFileSync(assetPathScss, assetContentScss, 'utf8'); }); it('should watch and rebuild assets and reload page in development (public)', async function() { await t.destroy(apos); let result = {}; const setTestResult = (obj) => { result = obj; }; const rootPath = process.cwd(); const assetPathJs = path.join(rootPath, 'test/modules/default-page/ui/public/index.js'); const assetPathCss = path.join(rootPath, 'test/modules/default-page/ui/public/index.css'); const assetPathPublicJs = path.join(rootPath, 'test/public/apos-frontend/default/public-module-bundle.js'); const assetPathPublicCss = path.join(rootPath, 'test/public/apos-frontend/default/public-bundle.css'); const assetPathAposJs = path.join(rootPath, 'test/public/apos-frontend/default/apos-module-bundle.js'); const assetPathAposCss = path.join(rootPath, 'test/public/apos-frontend/default/apos-bundle.css'); const assetContentJs = fs.readFileSync(assetPathJs, 'utf-8'); const assetContentScss = fs.readFileSync(assetPathCss, 'utf-8'); // Resurrect the default assets content if test has failed fs.writeFileSync(assetPathJs, assetContentJs, 'utf8'); fs.writeFileSync(assetPathCss, assetContentScss, 'utf8'); apos = await t.create({ root: module, autoBuild: true, modules: { 'default-page': {}, '@apostrophecms/asset': { extendMethods() { return { async watchUiAndRebuild(_super) { return _super(setTestResult); } }; } } } }); // Assert defaults const restartId = apos.asset.restartId; assert(apos.asset.buildWatcher); assert(apos.asset.restartId); assert(!result.builds); assert(!result.changes); // * modify assets and rebuild fs.writeFileSync( assetPathJs, 'export default () => { \'default-page-watcher-test-public\'; };\n', 'utf8' ); fs.writeFileSync( assetPathCss, '.default-page-watcher-test-public{color:red;}\n', 'utf8' ); // * change is in the public bundle await retryAssertTrue( async () => (await fs.readFile(assetPathPublicJs, 'utf8')).match(/default-page-watcher-test-public/), 'Unable to verify public JS asset was rebuilt by the watcher', 500, 10000 ); await retryAssertTrue( async () => (await fs.readFile(assetPathPublicCss, 'utf8')).match(/\.default-page-watcher-test-public/), 'Unable to verify public CSS asset was rebuilt by the watcher', 500, 10000 ); // * change is in the apos bundle await retryAssertTrue( async () => (await fs.readFile(assetPathAposJs, 'utf8')).match(/default-page-watcher-test-public/), 'Unable to verify apos JS asset was rebuilt by the watcher', 500, 10000 ); await retryAssertTrue( async () => (await fs.readFile(assetPathAposCss, 'utf8')).match(/\.default-page-watcher-test-public/), 'Unable to verify apos CSS asset was rebuilt by the watcher', 500, 10000 ); // * page has been restarted await retryAssertTrue( () => apos.asset.restartId !== restartId, 'Unable to verify restartId has been changed', 500, 10000 ); // * only public build was triggered await retryAssertTrue( () => result.builds.length === 1 && result.builds.includes('public'), 'Unable to verify build "public" has been triggered', 50, 1000 ); // * changes detected await retryAssertTrue( () => result.changes.length === 2 && result.changes .filter(f => (f.includes('modules/default-page/ui/public/index.js') || f.includes('modules/default-page/ui/public/index.css')) ) .length === 2, 'Unable to verify changes contain the proper source files', 50, 1000 ); await t.destroy(apos); assert.equal(apos.asset.buildWatcher, null); apos = null; fs.writeFileSync(assetPathJs, assetContentJs, 'utf8'); fs.writeFileSync(assetPathCss, assetContentScss, 'utf8'); }); it('should watch and rebuild assets and reload page in development (apos)', async function() { await t.destroy(apos); let result = {}; const setTestResult = (obj) => { result = obj; }; const rootPath = process.cwd(); const assetPathJs = path.join(rootPath, 'test/modules/default-page/ui/apos/components/FakeComponent.vue'); const assetPathAposJs = path.join(rootPath, 'test/public/apos-frontend/default/apos-module-bundle.js'); const assetContentJs = '<template><span /></template>\n'; fs.writeFileSync(assetPathJs, assetContentJs, 'utf8'); apos = await t.create({ root: module, autoBuild: true, modules: { 'default-page': { options: { components: { fake: 'FakeComponent' } } }, '@apostrophecms/asset': { extendMethods() { return { async watchUiAndRebuild(_super) { return _super(setTestResult); } }; } } } }); // Assert defaults const restartId = apos.asset.restartId; assert(apos.asset.buildWatcher); assert(apos.asset.restartId); assert(!result.builds); assert(!result.changes); // * modify assets and rebuild fs.writeFileSync( assetPathJs, '<template><span>default-page-watcher-test-apos</span></template>\n', 'utf8' ); // * change is in the apos bundle await retryAssertTrue( async () => (await fs.readFile(assetPathAposJs, 'utf8')) .includes('default-page-watcher-test-apos'), 'Unable to verify apos JS asset was rebuilt by the watcher', 500, 20000 ); // * page has been restarted await retryAssertTrue( () => apos.asset.restartId !== restartId, 'Unable to verify restartId has been changed', 500, 10000 ); // * only apos build was triggered await retryAssertTrue( () => result.builds.length === 1 && result.builds.includes('apos'), 'Unable to verify build "apos" has been triggered', 50, 1000 ); // * changes detected await retryAssertTrue( () => result.changes.length === 1 && result.changes[0].includes('modules/default-page/ui/apos/components/FakeComponent.vue'), 'Unable to verify changes contain the proper source files', 50, 1000 ); await t.destroy(apos); assert.equal(apos.asset.buildWatcher, null); apos = null; fs.writeFileSync(assetPathJs, assetContentJs, 'utf8'); }); it('should watch and recover after build error in development', async function() { await t.destroy(apos); let result = {}; let called = 0; const setTestResult = (obj) => { result = obj; called++; }; const rootPath = process.cwd(); const assetPathScss = path.join(rootPath, 'test/modules/default-page/ui/src/index.scss'); const assetPathPublicCss = path.join(rootPath, 'test/public/apos-frontend/default/public-bundle.css'); const assetPathAposCss = path.join(rootPath, 'test/public/apos-frontend/default/apos-bundle.css'); const assetContentScss = '.default-page {color:red;}\n'; // Resurrect the default assets content if test has failed fs.writeFileSync(assetPathScss, assetContentScss, 'utf8'); apos = await t.create({ root: module, autoBuild: true, modules: { 'default-page': {}, '@apostrophecms/asset': { extendMethods() { return { async watchUiAndRebuild(_super) { return _super(setTestResult); } }; } } } }); // Assert defaults const restartId = apos.asset.restartId; assert(apos.asset.buildWatcher); assert(apos.asset.restartId); assert(!result.builds); assert(!result.changes); // * modify assets and rebuild fs.writeFileSync( assetPathScss, 'bad code;\n', 'utf8' ); // * wait till the build ends await retryAssertTrue( () => called === 1 && result.builds.length === 0, 'Unable to verify build with error was triggered', 100, 10000 ); // * page has NOT been restarted await retryAssertTrue( () => apos.asset.restartId === restartId, 'Unable to verify restartId has been changed', 100, 10000 ); // * modify assets and recover fs.writeFileSync( assetPathScss, '.default-page-watcher-test-recover{color:red;}\n', 'utf8' ); // * change is in the public bundle await retryAssertTrue( async () => (await fs.readFile(assetPathPublicCss, 'utf8')).match(/\.default-page-watcher-test-recover/), 'Unable to verify public CSS asset was rebuilt by the watcher', 500, 10000 ); // * change is in the apos bundle await retryAssertTrue( async () => (await fs.readFile(assetPathAposCss, 'utf8')).match(/\.default-page-watcher-test-recover/), 'Unable to verify apos CSS asset was rebuilt by the watcher', 500, 10000 ); // * page has been restarted await retryAssertTrue( () => apos.asset.restartId !== restartId, 'Unable to verify restartId has been changed', 500, 10000 ); // * only src related builds were triggered await retryAssertTrue( () => result.builds.length === 1 && result.builds.includes('src'), 'Unable to verify build "src" have been triggered', 50, 1000 ); // * changes detected await retryAssertTrue( () => result.changes.length === 1 && result.changes .filter(f => (f.includes('modules/default-page/ui/src/index.scss')) ) .length === 1, 'Unable to verify changes contain the proper source files', 50, 1000 ); await t.destroy(apos); assert.equal(apos.asset.buildWatcher, null); apos = null; fs.writeFileSync(assetPathScss, assetContentScss, 'utf8'); }); it('should watch but not rebuild assets and not reload page when changes are not in use', async function() { await t.destroy(apos); let result = {}; let rebuilt = false; const setTestResult = (obj) => { result = obj; rebuilt = true; }; const rootPath = process.cwd(); const assetPathJs = path.join(rootPath, 'test/modules/default-page/ui/src/index.js'); const assetPathScss = path.join(rootPath, 'test/modules/default-page/ui/src/index.scss'); const assetPathPublicJs = path.join(rootPath, 'test/public/apos-frontend/default/public-module-bundle.js'); const assetPathPublicCss = path.join(rootPath, 'test/public/apos-frontend/default/public-bundle.css'); const assetPathAposJs = path.join(rootPath, 'test/public/apos-frontend/default/apos-module-bundle.js'); const assetPathAposCss = path.join(rootPath, 'test/public/apos-frontend/default/apos-bundle.css'); const assetContentJs = fs.readFileSync(assetPathJs, 'utf-8'); const assetContentScss = fs.readFileSync(assetPathScss, 'utf-8'); // Resurrect the default assets content if test has failed fs.writeFileSync(assetPathJs, assetContentJs, 'utf8'); fs.writeFileSync(assetPathScss, assetContentScss, 'utf8'); apos = await t.create({ root: module, autoBuild: true, modules: { '@apostrophecms/asset': { extendMethods() { return { async watchUiAndRebuild(_super) { return _super(setTestResult); } }; } } } }); // Assert defaults const restartId = apos.asset.restartId; assert(apos.asset.buildWatcher); assert(apos.asset.restartId); assert(!result.builds); assert(!result.changes); assert.equal(rebuilt, false); // * modify assets fs.writeFileSync( assetPathJs, 'export default () => { \'default-page-watcher-test-src\'; };\n', 'utf8' ); fs.writeFileSync( assetPathScss, '.default-page-watcher-test-src{color:red;}\n', 'utf8' ); // * rebuild handler has NOT been triggered // This changes now because the watcher listens ONLY // for registered modules. await retryAssertTrue( () => rebuilt === false, 'Unable to verify rebuild has NOT been triggered', 500, 10000 ); // * change is NOT in the public bundle await retryAssertTrue( async () => !(await fs.readFile(assetPathPublicJs, 'utf8')).match(/default-page-watcher-test-src/), 'Unable to verify public JS asset was NOT rebuilt by the watcher', 500, 1000 ); await retryAssertTrue( async () => !(await fs.readFile(assetPathPublicCss, 'utf8')).match(/\.default-page-watcher-test-src/), 'Unable to verify public CSS asset was NOT rebuilt by the watcher', 500, 1000 ); // * change is NOT in the apos bundle await retryAssertTrue( async () => !(await fs.readFile(assetPathAposJs, 'utf8')).match(/default-page-watcher-test-src/), 'Unable to verify apos JS asset was NOT rebuilt by the watcher', 500, 1000 ); await retryAssertTrue( async () => !(await fs.readFile(assetPathAposCss, 'utf8')).match(/\.default-page-watcher-test-src/), 'Unable to verify apos CSS asset was NOT rebuilt by the watcher', 500, 1000 ); // * page has NOT been restarted await retryAssertTrue( () => apos.asset.restartId === restartId, 'Unable to verify restartId has NOT been changed', 500, 1000 ); // * no builds were triggered // A test change because we don't watch this location at all, // so no chokidar trigger. await retryAssertTrue( () => typeof result.builds === 'undefined', 'Unable to verify build "src" has NOT been triggered', 50, 1000 ); // Outdated, changes not detected because the watcher got smarter. // * changes detected // await retryAssertTrue( // () => // result.changes.length === 2 && // result.changes // .filter(f => // (f.includes('modules/default-page/ui/src/index.js') || // f.includes('modules/default-page/ui/src/index.scss')) // ) // .length === 2, // 'Unable to verify changes contain the proper source files', // 50, // 1000 // ); await t.destroy(apos); assert.equal(apos.asset.buildWatcher, null); apos = null; fs.writeFileSync(assetPathJs, assetContentJs, 'utf8'); fs.writeFileSync(assetPathScss, assetContentScss, 'utf8'); }); it('should watch and rebuild assets in a debounced queue', async function() { await t.destroy(apos); let timesRebuilt = 0; const inc = () => { timesRebuilt += 1; }; apos = await t.create({ root: module, autoBuild: true, modules: { ...modules, '@apostrophecms/asset': { extendMethods() { return { async watchUiAndRebuild(_super) { return _super(inc); } }; } } } }); assert(apos.asset.buildWatcher); const assetPath = path.join(process.cwd(), 'test/modules/bundle-page/ui/src/extra.js'); const assetPathPublic = path.join(process.cwd(), 'test/public/apos-frontend/default/extra-module-bundle.js'); const assetContent = fs.readFileSync(assetPath, 'utf-8'); // Modify below the debounce rate for (const i of [ 1, 2, 3 ]) { await fs.writeFile( assetPath, `export default () => { 'bundle-page-watcher-test-${i}'; };\n`, 'utf8' ); await Promise.delay(300); } await retryAssertTrue( async () => (await fs.readFile(assetPathPublic, 'utf8')).match(/bundle-page-watcher-test-3/), 'Unable to verify public asset rebuilding by the watcher', 500, 10000 ); await retryAssertTrue( () => timesRebuilt === 1, `Expected to rebuild 1 time, got ${timesRebuilt}`, 100, 5000 ); // Modify above the debounce rate, test the queue cap timesRebuilt = 0; for (const i of [ 1, 2, 3 ]) { await fs.writeFile( assetPath, `export default () => { 'bundle-page-watcher-test-${i}0'; };\n`, 'utf8' ); await Promise.delay(1050); } await retryAssertTrue( async () => (await fs.readFile(assetPathPublic, 'utf8')).match(/bundle-page-watcher-test-30/), 'Unable to verify public asset rebuilding by the watcher', 500, 10000 ); await retryAssertTrue( () => timesRebuilt === 3, `Expected to rebuild 3 times, got ${timesRebuilt}`, 100, 5000 ); await t.destroy(apos); apos = null; fs.writeFileSync(assetPath, assetContent, 'utf8'); }); it('should be able to setup the debounce time', async function() { apos = await t.create({ root: module, modules: { '@apostrophecms/asset': { options: { watchDebounceMs: 500 } } } }); assert.equal(apos.asset.buildWatcherDebounceMs, 500); }); it('should be able to register an external build watcher', async function() { await t.destroy(apos); const chokidar = require('chokidar'); let instance; apos = await t.create({ root: module, autoBuild: true, modules: { '@apostrophecms/asset': { methods(self) { return { registerBuildWatcher() { self.buildWatcher = chokidar.watch([ __filename ], { cwd: self.apos.rootDir, ignoreInitial: true }); instance = self.buildWatcher; } }; } } } }); assert.equal(apos.asset.buildWatcher, instance); }); it('should not watch if explicitly disabled by option or env in development', async function() { await t.destroy(apos); process.env.APOS_ASSET_WATCH = '0'; apos = await t.create({ root: module, autoBuild: true }); assert(!apos.asset.buildWatcher); delete process.env.APOS_ASSET_WATCH; await t.destroy(apos); apos = await t.create({ root: module, autoBuild: true, modules: { '@apostrophecms/asset': { options: { watch: false } } } }); assert(!apos.asset.buildWatcher); }); it('should not watch if autoBuild is disabled', async function() { await t.destroy(apos); apos = await t.create({ root: module }); assert(!apos.asset.buildWatcher); }); it('should not watch in production', async function() { await t.destroy(apos); process.env.NODE_ENV = 'production'; apos = await t.create({ root: module, autoBuild: true, modules }); assert(!apos.asset.buildWatcher); process.env.NODE_ENV = 'development'; await t.destroy(apos); }); it('should pass the right options to webpack extensions from all modules', async function() { const { extConfig1, extConfig2 } = getWebpackConfigsForExtensionOptions(); apos = await t.create({ root: module, modules: { 'test-widget': { extend: '@apostrophecms/widget-type', webpack: extConfig1 }, test: { extend: '@apostrophecms/piece-type', webpack: extConfig2 } } }); const { extensions, extensionOptions } = await getWebpackExtensions({ name: 'src', getMetadata: apos.synth.getMetadata, modulesToInstantiate: apos.modulesToBeInstantiated() }); assertWebpackExtensionOptions(extensions, extensionOptions); await t.destroy(apos); }); it('should allow two modules extending each others to pass options to the same webpack extension', async function() { const { extConfig1, extConfig2 } = getWebpackConfigsForExtensionOptions(); apos = await t.create({ root: module, modules: { 'test-widget': { extend: '@apostrophecms/widget-type', instantiate: false, webpack: extConfig1 }, 'test-widget-special': { extend: 'test-widget', webpack: extConfig2 } } }); assert(!apos.modules['test-widget']); const { extensions, extensionOptions } = await getWebpackExtensions({ name: 'src', getMetadata: apos.synth.getMetadata, modulesToInstantiate: apos.modulesToBeInstantiated() }); assertWebpackExtensionOptions(extensions, extensionOptions); }); it('should verify that asset re-bundle configs are valid', async function () { assert.doesNotThrow(() => verifyRebundleConfig()); assert.doesNotThrow(() => verifyRebundleConfig([])); assert.doesNotThrow(() => formatRebundleConfig()); assert.doesNotThrow(() => formatRebundleConfig({})); assert.doesNotThrow(() => formatRebundleConfig({ 'bundle-page': 'main', 'bundle-page-type': 'new', 'bundle-widget:extra': 'widget', '@company/bundle:company': 'newcompany', 'bundle-edge:edge': 'main' })); // too much catch-all assert.throws(() => formatRebundleConfig({ 'bundle-page': 'main', 'bundle-page:extra': 'new' })); assert.throws(() => formatRebundleConfig({ 'bundle-page:extra': 'new', 'bundle-page': 'main' })); assert.throws(() => formatRebundleConfig({ 'bundle-page': 'main', 'bundle-page:extra': 'main' })); assert.throws(() => formatRebundleConfig({ 'bundle-page': 'new', 'bundle-page:extra': 'another' })); assert.throws(() => formatRebundleConfig({ 'bundle-page:extra': 'another', 'bundle-page': 'new' })); }); it('should build and remap the right bundles in dev and prod modes', async function () { await t.destroy(apos); const getPath = (p) => `${publicFolderPath}/apos-frontend/` + p; const checkFileExists = async (p) => fs.pathExists(getPath(p)); async function checkBundlesExists (folderPath, fileNames) { for (const fileName of fileNames) { const extraBundleExists = await checkFileExists(folderPath + fileName); assert(extraBundleExists); } } function checkBundlesContents (folderPath, bundles, not = false) { for (const [ fileName, regexes ] of Object.entries(bundles)) { const contents = fs.readFileSync(getPath(folderPath + fileName), 'utf-8'); for (const regex of regexes) { const method = not ? 'doesNotMatch' : 'match'; assert[method](contents, new RegExp(regex), `${fileName} - ${regex}`); } } } apos = await t.create({ root: module, modules: { '@company/bundle': {}, 'bundle-edge': {}, ...modules, '@apostrophecms/asset': { options: { rebundleModules: { // Everything from the `bundle-page` module should // go in the regular "main" bundle 'bundle-page': 'main', // all from `bundle-page-type` should go // in a new bundle 'bundle-page' 'bundle-page-type': 'new', // 'extra2' bundle from `bundle-widget` should go // in a new bundle 'widget-bundle' 'bundle-widget:extra2': 'widget', // 'company' bundle from `@company/bundle:company` should go // in a new bundle 'newcompany'. The local module contribution // to the 'company' build should stay. '@company/bundle:company': 'newcompany', // Edge case - send "edge" bundle only to the main bundle 'bundle-edge:edge': 'main' } } } } }); const existingBundleNames = [ 'public-module-bundle.js', 'public-bundle.css', 'new-module-bundle.js', 'new-bundle.css', 'company-module-bundle.js', 'company-bundle.css', 'newcompany-module-bundle.js', 'newcompany-bundle.css', 'widget-module-bundle.js' ]; const bundleContents = { 'public-module-bundle.js': [ /BUNDLE_MAIN_PAGE['"]+/g, /BUNDLE_EXTRA_PAGE['"]+/g, /BUNDLE_EDGE['"]+/g ], 'public-bundle.css': [ /\.main-page[\s]*\{/g, /\.extra-page[\s]*\{/g, /\.edge[\s]*\{/g ], 'new-module-bundle.js': [ /BUNDLE_INDEX_PAGE_TYPE['"]+/g, /BUNDLE_ANOTHER_PAGE_TYPE['"]+/g, /BUNDLE_MAIN_PAGE_TYPE['"]+/g ], 'new-bundle.css': [ /\.index-page-type[\s]*\{/g, /\.main-page-type[\s]*\{/g ], 'company-module-bundle.js': [ /BUNDLE_OVERRIDE_COMPANY['"]+/g ], 'company-bundle.css': [ /\.override-company[\s]*\{/g ], 'newcompany-module-bundle.js': [ /BUNDLE_COMPANY['"]+/g ], 'newcompany-bundle.css': [ /\.company[\s]*\{/g ], 'widget-module-bundle.js': [ /BUNDLE_WIDGET_EXTRA2['"]+/g ] }; const bundleNoDuplicateContents = { 'public-module-bundle.js': [ /BUNDLE_COMPANY['"]+/g, /BUNDLE_WIDGET_EXTRA2['"]+/g, /BUNDLE_OVERRIDE_COMPANY['"]+/g, /BUNDLE_MAIN_PAGE_TYPE['"]+/g, /BUNDLE_ANOTHER_PAGE_TYPE['"]+/g, /BUNDLE_INDEX_PAGE_TYPE['"]+/g ], 'public-bundle.css': [ /\.main-page-type[\s]*\{/g, /\.override-company[\s]*\{/g, /\.company[\s]*\{/g ] }; const nonExistingBundleNames = [ 'main-module-bundle.js', 'extra-module-bundle.js', 'extra2-module-bundle.js', 'edge-module-bundle.js', 'main-bundle.css', 'extra-bundle.css', 'edge-bundle.css' ]; process.env.NODE_ENV = 'production'; await deleteBuiltFolders(publicFolderPath, true); await apos.asset.tasks.build.task(); { const [ releaseId ] = await fs.readdir(getPath('releases')); const releasePath = `releases/${releaseId}/default/`; await checkBundlesExists(releasePath, existingBundleNames); checkBundlesContents(releasePath, bundleContents); checkBundlesContents(releasePath, bundleNoDuplicateContents, true); for (const file of nonExistingBundleNames) { assert.throws(() => fs.readFileSync(releasePath + file), { code: 'ENOENT' }, file); } } process.env.NODE_ENV = 'development'; await deleteBuiltFolders(publicFolderPath, true); await apos.asset.tasks.build.task(); { const releasePath = getPath('default/'); await checkBundlesExists('default/', existingBundleNames); checkBundlesContents('default/', bundleContents); checkBundlesContents('default/', bundleNoDuplicateContents, true); for (const file of nonExistingBundleNames) { assert.throws(() => fs.readFileSync(releasePath + file), { code: 'ENOENT' }, file); } } }); it('should load the right remapped bundles inside the right page', async function () { await t.destroy(apos); apos = await t.create({ root: module, modules: { '@company/bundle': {}, 'bundle-edge': {}, ...modules, '@apostrophecms/asset': { options: { rebundleModules: { 'bundle-page': 'main', 'bundle-page-type': 'new', 'bundle-widget:extra2': 'widget', '@company/bundle:company': 'newcompany' } } } } }); const { _id: homeId } = await apos.page .find(apos.task.getAnonReq(), { level: 0 }) .toObject(); const jar = apos.http.jar(); await apos.doc.db.insertMany(pagesToInsert(homeId)); const bundlePage = await apos.http.get('/bundle', { jar }); assert(bundlePage.includes(getStylesheetMarkup('public-bundle'))); assert(bundlePage.includes(getStylesheetMarkup('new-bundle'))); assert(!bundlePage.includes(getStylesheetMarkup('main-bundle'))); assert(!bundlePage.includes(getStylesheetMarkup('extra-bundle'))); assert(!bundlePage.includes(getStylesheetMarkup('extra2-bundle'))); assert(!bundlePage.includes(getStylesheetMarkup('newcompany-bundle'))); assert(!bundlePage.includes(getStylesheetMarkup('company-bundle'))); assert(bundlePage.includes(getScriptMarkup('public-module-bundle'))); assert(bundlePage.includes(getScriptMarkup('new-module-bundle'))); assert(bundlePage.includes(getScriptMarkup('widget-module-bundle'))); assert(!bundlePage.includes(getScriptMarkup('main-module-bundle'))); assert(!bundlePage.includes(getScriptMarkup('extra-module-bundle'))); assert(!bundlePage.includes(getScriptMarkup('extra2-module-bundle'))); assert(!bundlePage.includes(getStylesheetMarkup('newcompany-bundle'))); assert(!bundlePage.includes(getStylesheetMarkup('company-bundle'))); const childPage = await apos.http.get('/bundle/child', { jar }); assert(childPage.includes(getStylesheetMarkup('public-bundle'))); assert(childPage.includes(getStylesheetMarkup('new-bundle'))); assert(!childPage.includes(getStylesheetMarkup('main-bundle'))); assert(!childPage.includes(getStylesheetMarkup('extra-bundle'))); assert(!childPage.includes(getStylesheetMarkup('extra2-bundle'))); assert(childPage.includes(getScriptMarkup('public-module-bundle'))); assert(childPage.includes(getScriptMarkup('new-module-bundle'))); assert(!childPage.includes(getScriptMarkup('widget-module-bundle'))); assert(!childPage.includes(getScriptMarkup('main-module-bundle'))); assert(!childPage.includes(getScriptMarkup('extra-module-bundle'))); assert(!childPage.includes(getScriptMarkup('extra2-module-bundle'))); }); it('should load all the remapped bundles on all pages when the user is logged in', async function () { await t.createAdmin(apos); const jar = await t.getUserJar(ap