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
42 changes: 31 additions & 11 deletions lib/BuildConfigManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { once } from 'lodash-es';
import { fork } from 'child_process';
import path from 'path';
import fsModule from 'fs';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
import { THEME_PATH } from '../constants.js';

const require = createRequire(import.meta.url);
const isJest = typeof process !== 'undefined' && 'JEST_WORKER_ID' in process.env;

const cornerstoneConfigLink = isJest
Expand All @@ -16,8 +15,10 @@ class BuildConfigManager {
constructor({ workDir = THEME_PATH, fs = fsModule, timeout = 20000 } = {}) {
this.oldConfigFileName = 'stencil.conf.js';
this.configFileName = 'stencil.conf.cjs';
this.esmConfigFileName = 'stencil.conf.mjs';
this._workDir = workDir;
this._buildConfigPath = path.join(workDir, this.configFileName);
this._esmBuildConfigPath = path.join(workDir, this.esmConfigFileName);
this._oldBuildConfigPath = path.join(workDir, this.oldConfigFileName);
this._fs = fs;
this._onReadyCallbacks = [];
Expand All @@ -27,15 +28,19 @@ class BuildConfigManager {
}

async initConfig() {
const config = await this._getConfig(this._buildConfigPath, this._oldBuildConfigPath);
const config = await this._getConfig(
this._resolveBuildConfigPath(),
this._oldBuildConfigPath,
);
this.development = config.development || this._devWorker;
this.production = config.production || this._prodWorker;
this.watchOptions = config.watchOptions;
}

initWorker() {
if (this._fs.existsSync(this._buildConfigPath)) {
this._worker = fork(this._buildConfigPath, [], { cwd: this._workDir });
const buildConfigPath = this._resolveBuildConfigPath();
if (this._fs.existsSync(buildConfigPath)) {
this._worker = fork(buildConfigPath, [], { cwd: this._workDir });
this._worker.on('message', (message) => {
if (message === 'ready') {
this._workerIsReady = true;
Expand All @@ -50,23 +55,38 @@ class BuildConfigManager {
this._worker.kill(signal);
}

// Prefer an ESM stencil.conf.mjs when the theme provides one; otherwise
// use the CommonJS stencil.conf.cjs (which legacy stencil.conf.js files
// get renamed to)
_resolveBuildConfigPath() {
if (this._fs.existsSync(this._esmBuildConfigPath)) {
return this._esmBuildConfigPath;
}
return this._buildConfigPath;
}

async _getConfig(configPath, oldConfigPath) {
if (this._fs.existsSync(configPath)) {
// eslint-disable-next-line import/no-dynamic-require
return require(configPath);
return this._importConfig(configPath);
}
if (this._fs.existsSync(oldConfigPath)) {
this._moveOldConfig(configPath, oldConfigPath);
// eslint-disable-next-line import/no-dynamic-require
return require(configPath);
return this._importConfig(configPath);
}

// Fallback to cornerstone default stencil.conf.cjs
const content = await this.downloadFileFromGitHub(cornerstoneConfigLink);
this._fs.writeFileSync(configPath, content);

// eslint-disable-next-line import/no-dynamic-require
return require(configPath);
return this._importConfig(configPath);
}

// Dynamic import() loads both ESM and CommonJS configs. CommonJS exposes
// module.exports as the default export; ESM configs use named exports
async _importConfig(configPath) {
// eslint-disable-next-line node/no-unsupported-features/es-syntax
const configModule = await import(pathToFileURL(configPath).href);
return configModule.default ?? configModule;
}

async downloadFileFromGitHub(rawUrl) {
Expand Down
28 changes: 28 additions & 0 deletions lib/BuildConfigManager.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ describe('BuildConfigManager integration tests', () => {
expect(buildConfig.watchOptions.files).toBeInstanceOf(Array);
expect(buildConfig.watchOptions.ignored).toBeInstanceOf(Array);
});
it('should prefer stencil.conf.mjs over stencil.conf.cjs when both exist', async () => {
const buildConfig = new BuildConfigManager({
workDir: `${cwd}/test/_mocks/build-config/esm-config`,
});
await buildConfig.initConfig();
expect(buildConfig.watchOptions.files).toEqual(['/templates', '/lang']);
});
});
describe('production method', () => {
it('should resolve successfully for "valid-config"', async () => {
Expand All @@ -40,6 +47,16 @@ describe('BuildConfigManager integration tests', () => {
await promisify(buildConfig.production.bind(buildConfig))();
buildConfig.stopWorker();
});
it('should resolve successfully for "esm-config"', async () => {
const buildConfig = new BuildConfigManager({
workDir: `${cwd}/test/_mocks/build-config/esm-config`,
});
await buildConfig.initConfig();
buildConfig.initWorker();
expect(buildConfig.production).toBeInstanceOf(Function);
await promisify(buildConfig.production.bind(buildConfig))();
buildConfig.stopWorker();
});
it('should reject with "worker terminated" message for "noworker-config"', async () => {
const buildConfig = new BuildConfigManager({
workDir: `${cwd}/test/_mocks/build-config/noworker-config`,
Expand Down Expand Up @@ -93,5 +110,16 @@ describe('BuildConfigManager integration tests', () => {
});
buildConfig.stopWorker();
});
it('should reload the browser when a message "reload" is received from stencil.conf.mjs (esm-config)', async () => {
const buildConfig = new BuildConfigManager({
workDir: `${cwd}/test/_mocks/build-config/esm-config`,
});
await buildConfig.initConfig();
expect(buildConfig.development).toBeInstanceOf(Function);
await new Promise((done) => {
buildConfig.initWorker().development({ reload: done });
});
buildConfig.stopWorker();
});
});
});
1 change: 1 addition & 0 deletions lib/stencil-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const PATHS_TO_ZIP = [
{ pattern: '.scss-lint.yml' },
{ pattern: 'stencil.conf.cjs' },
{ pattern: 'stencil.conf.js' },
{ pattern: 'stencil.conf.mjs' },
{ pattern: 'templates/**/*' },
{ pattern: 'webpack.*.js' },
];
Expand Down
9 changes: 9 additions & 0 deletions test/_mocks/build-config/esm-config/stencil.conf.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Decoy config: stencil.conf.mjs in this directory must take precedence.
// If the CLI loads or forks this file instead, the esm-config tests fail
// (wrong watchOptions, and no worker protocol so the fork exits immediately).
const watchOptions = {
files: ['/cjs-config-should-not-be-loaded'],
ignored: [],
};

module.exports = { watchOptions };
50 changes: 50 additions & 0 deletions test/_mocks/build-config/esm-config/stencil.conf.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Watch options for the core watcher
* @type {{files: string[], ignored: string[]}}
*/
// eslint-disable-next-line import/prefer-default-export
export const watchOptions = {
// If files in these directories change, reload the page.
files: [
'/templates',
'/lang',
],

// Do not watch files in these directories
ignored: [
'/assets/scss',
'/assets/css',
'/assets/dist',
],
};

/**
* Watch any custom files and trigger a rebuild
*/
function development() {
// Rebuild the bundle once at bootup
setTimeout(() => process.send('reload'), 10);
}

/**
* Hook into the `stencil bundle` command and build your files before they are packaged as a .zip
*/
function production() {
// Rebuild the bundle once at bootup
setTimeout(() => process.send('done'), 10);
}

if (process.send) {
// running as a forked worker
process.on('message', message => {
if (message === 'development') {
development();
}

if (message === 'production') {
production();
}
});

process.send('ready');
}
Loading