Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:
node-version: ${{ matrix.node }}
- name: Install dependencies
run: npm install
- name: Lint
run: npm run lint
- name: Testing
run: npm test
sonar:
Expand Down
2 changes: 1 addition & 1 deletion components/src/widgets/complexTable/widget.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ describe('ComplexTable widget', () => {

await nextTick()

expect(wrapper.emitted('itemsLoaded'))
expect(wrapper.emitted().itemsLoaded).toBeTruthy();
});
});
});
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"build:tools": "NODE_ENV=production webpack --config ./tools/webpack.config.js",
"start": "NODE_ENV=development webpack serve --config ./webpack-dev.config.js",
"start:https": "npm run start -- --server-type https",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --ignore-path .gitignore",
"lint:fix": "npm run lint -- --fix",
"test": "jest --config ./jest.config.js",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
Expand Down
16 changes: 16 additions & 0 deletions tools/build/vite/flatten-html-pages-directory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default {
// Custom plugin to flatten the output directory structure for the extension pages
// Vite does not respect the name given to the input file (see https://vitejs.dev/guide/build.html#multi-page-app)
// for decent reasons, but these reasons do not apply to Connect Extensions, as they are not run on dev mode.
// See https://stackoverflow.com/a/77096400 for more info on this solution
name: 'flatten-html-pages-directory',
enforce: 'post',
generateBundle(_, bundle) {
Object.values(bundle).forEach((outputItem) => {
if (outputItem.fileName.endsWith('.html')) {
const pageName = outputItem.fileName.match(/([\w\-_]+)\/index\.html/)[1];
outputItem.fileName = `${pageName}.html`;
}
});
},
}
30 changes: 30 additions & 0 deletions tools/build/vite/flatten-html-pages-directory.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import flattenHtmlPagesDirectory from './flatten-html-pages-directory';


describe('#flattenHtmlPagesDirectory vite plugin', () => {
it('exposes the correct properties', () => {
expect(flattenHtmlPagesDirectory).toEqual(expect.objectContaining({
name: 'flatten-html-pages-directory',
enforce: 'post',
generateBundle: expect.any(Function),
}));
});

describe('generateBundle function', () => {
it('changes the fileName of items that are html files and leaves the rest as they are', () => {
const bundle = {
foo: { fileName: 'one/two/index.js', id: 'foo' },
bar: { fileName: 'three/four/index.html', id: 'bar' },
baz: { fileName: 'five/six/index.css', id: 'baz' },
};

flattenHtmlPagesDirectory.generateBundle(null, bundle);

expect(bundle).toEqual({
foo: { fileName: 'one/two/index.js', id: 'foo' },
bar: { fileName: 'four.html', id: 'bar' },
baz: { fileName: 'five/six/index.css', id: 'baz' },
});
});
});
});
87 changes: 87 additions & 0 deletions tools/build/vite/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { fileURLToPath } from 'node:url';
import { readdirSync } from 'node:fs';
import { resolve } from 'node:path';

import flattenHtmlPagesDirectoryPlugin from './flatten-html-pages-directory';


/**
* Creates a valid vite config set up for a Connect extension that uses Vite + Vue
*
* @param {object} config - main configuration object
* @param {string} config.srcDir - absolute path for the src folder
* @param {URL} config.srcUrl - URL for the src folder, used for aliasing '~'
* @param {string} config.outputDir - absolute path for the output directory
* @param {object} config.vuePlugin - '@vitejs/vue' plugin instance
* @param {object} viteOptions - your custom vite config options
*
* @returns {object} - Valid vite config set up for a connect extension
*/
export const defineExtensionConfig = (config, viteOptions = {}) => {
const {
srcDir,
srcUrl,
outputDir,
vuePlugin,
} = config;

if (!srcDir) throw new Error('"srcDir" is required');
if (!outputDir) throw new Error('"outputDir" is required');
if (!vuePlugin) throw new Error('"vuePlugin" is required');
if (!srcUrl) throw new Error('"srcUrl" is required');


return {
...viteOptions,

resolve: {
...viteOptions.resolve,

alias: {
...viteOptions.resolve?.alias,

'~': fileURLToPath(srcUrl),
},
},

plugins: [
vuePlugin,
flattenHtmlPagesDirectoryPlugin,
...(viteOptions.plugins || []),
],

root: srcDir,
base: '/static',

build: {
...viteOptions.build,

outDir: outputDir,
emptyOutDir: true,

rollupOptions: {
...viteOptions.build?.rollupOptions,

// Load all pages in {{srcDir}}/pages/{{pageName}}/index.html as entrypoints
input: readdirSync(resolve(srcDir, 'pages')).reduce((entryPoints, pageName) => {
entryPoints[pageName] = resolve(srcDir, 'pages/', pageName, 'index.html');

return entryPoints;
}, {}),

output: {
...viteOptions.build?.rollupOptions?.output,

format: 'es',
dir: outputDir,

// Split node_modules into a "vendor" chunk, and @cloudblueconnect modules into a "connect" chunk
manualChunks(id) {
if (id.includes('@cloudblueconnect')) return 'connect';
if (id.includes('node_modules')) return 'vendor';
},
},
},
},
};
};
230 changes: 230 additions & 0 deletions tools/build/vite/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { readdirSync } from 'node:fs';
import { resolve } from 'node:path';

import { defineExtensionConfig } from './index';


jest.mock('./flatten-html-pages-directory', () => 'flattenHtmlPagesDirectoryPluginStub');

jest.mock('node:url', () => ({
fileURLToPath: jest.fn().mockReturnValue('urlFileUrlToPathStub'),
}));

jest.mock('node:fs', () => ({
readdirSync: jest.fn().mockReturnValue(['fsReaddirSyncStub']),
}));

jest.mock('node:path', () => ({
resolve: jest.fn().mockReturnValue('pathResolveStub'),
}));

describe('#defineExtensionConfig function', () => {
let result;

describe('required options', () => {
it.each([
// expectedErrorMessage, config
[
'"srcDir" is required',
{
srcDir: undefined,
srcUrl: 'bar',
outputDir: 'baz',
vuePlugin: 'qux',
},
],
[
'"srcUrl" is required',
{
srcDir: 'foo',
srcUrl: undefined,
outputDir: 'baz',
vuePlugin: 'qux',
},
],
[
'"outputDir" is required',
{
srcDir: 'foo',
srcUrl: 'bar',
outputDir: undefined,
vuePlugin: 'qux',
},
],
[
'"vuePlugin" is required',
{
srcDir: 'foo',
srcUrl: 'bar',
outputDir: 'baz',
vuePlugin: undefined,
},
],
])(
'throws an error with the message %s if config=%o',
(expectedErrorMessage, config) => {
let error;

try {
defineExtensionConfig(config);
} catch (e) {
error = e;
}

expect(error.message).toEqual(expectedErrorMessage);
},
);
});

it('returns the base config', () => {
const config = {
srcDir: '/my/source/dir',
srcUrl: 'file://my/source/dir',
outputDir: '/my/output/dir',
vuePlugin: { name: 'vuepluginstub' },
};

result = defineExtensionConfig(config);

expect(result).toEqual({
resolve: {
alias: {
'~': 'urlFileUrlToPathStub',
},
},
plugins: [
{ name: 'vuepluginstub' },
'flattenHtmlPagesDirectoryPluginStub',
],
root: '/my/source/dir',
base: '/static',
build: {
outDir: '/my/output/dir',
emptyOutDir: true,
rollupOptions: {
input: {
fsReaddirSyncStub: 'pathResolveStub',
},
output: {
format: 'es',
dir: '/my/output/dir',
manualChunks: expect.any(Function),
},
},
},
});
});

it('returns the base config merged with a custom Vite config', () => {
const config = {
srcDir: '/my/source/dir',
srcUrl: 'file://my/source/dir',
outputDir: '/my/output/dir',
vuePlugin: { name: 'vuepluginstub' },
};

const customViteConfig = {
foo: 'bar',
resolve: {
one: 'two',
alias: {
'@': '/some/path',
},
},
plugins: ['other-vite-plugin'],
build: {
someProperty: 'someValue',
rollupOptions: {
bar: 'baz',
output: {
baz: 'qux',
},
},
},
};

result = defineExtensionConfig(config, customViteConfig);

expect(result).toEqual({
foo: 'bar',
resolve: {
one: 'two',
alias: {
'~': 'urlFileUrlToPathStub',
'@': '/some/path',
},
},
plugins: [
{ name: 'vuepluginstub' },
'flattenHtmlPagesDirectoryPluginStub',
'other-vite-plugin'
],
root: '/my/source/dir',
base: '/static',
build: {
someProperty: 'someValue',
outDir: '/my/output/dir',
emptyOutDir: true,
rollupOptions: {
bar: 'baz',
input: {
fsReaddirSyncStub: 'pathResolveStub',
},
output: {
baz: 'qux',
format: 'es',
dir: '/my/output/dir',
manualChunks: expect.any(Function),
},
},
},
});
});

it('does proper input entrypoints resolution', () => {
const config = {
srcDir: '/my/source/dir',
srcUrl: 'file://my/source/dir',
outputDir: '/my/output/dir',
vuePlugin: { name: 'vuepluginstub' },
};

result = defineExtensionConfig(config);

expect(resolve).toHaveBeenCalledWith('/my/source/dir', 'pages');
expect(readdirSync).toHaveBeenCalledWith('pathResolveStub');
expect(resolve).toHaveBeenCalledWith('/my/source/dir', 'pages/', 'fsReaddirSyncStub', 'index.html');
expect(result.build.rollupOptions.input).toEqual({
fsReaddirSyncStub: 'pathResolveStub',
});
});

describe('#config.build.rollupOptions.output.manualChunks', () => {
it.each([
// expected, moduleId
['connect', 'foo/bar/@cloudblueconnect/material-svg/baseline/googlePhoneBaseline.svg'],
['connect', 'foo/bar/@cloudblueconnect/connect-ui-toolkit/tools/vue/toolkitPlugin.js'],
['connect', 'node_modules/@cloudblueconnect/connect-ui-toolkit/index.js'],
['vendor', 'node_modules/@cloudgreendisconnect/disconnect-backend-toolkit/index.js'],
['vendor', 'node_modules/vue/index.js'],
['vendor', 'foo/bar/baz/node_modules/vuex/index.js'],
[undefined, 'foo/bar/baz/index.js'],
[undefined, 'main.css'],
])(
'returns %s if the module id=%s',
(expected, moduleId) => {
const config = {
srcDir: '/my/source/dir',
srcUrl: 'file://my/source/dir',
outputDir: '/my/output/dir',
vuePlugin: { name: 'vuepluginstub' },
};
const manualChunksFn = defineExtensionConfig(config).build.rollupOptions.output.manualChunks;

result = manualChunksFn(moduleId);

expect(result).toEqual(expected);
},
);
});
});
Loading