Skip to content

Commit

Permalink
feat(workspace-plugin): implement build executor (#32439)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hotell authored Sep 24, 2024
1 parent 6cfb9c8 commit 20bfbad
Show file tree
Hide file tree
Showing 22 changed files with 878 additions and 18 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ dist
dist-storybook
screenshots
!tools/**/lib
tools/**/__fixtures__/**/lib
tools/**/__fixtures__/**/lib-commonjs

*.tar.gz

Expand Down
10 changes: 8 additions & 2 deletions tools/workspace-plugin/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ export default {
displayName: 'workspace-plugin',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': ['@swc/jest', {}],
'^.+\\.[tj]s$': ['@swc/jest', { exclude: '/node_modules/' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
/**
* NOTE: because @swc/types ships index.ts and has not properly set up the package.json "types" field, if we set 'ts' first, it will try to use the index.ts file and fail.
*/
moduleFileExtensions: ['js', 'ts', 'html'],
testPathIgnorePatterns: ['/node_modules/', '/__fixtures__/'],
transformIgnorePatterns: ['/node_modules/', '/__fixtures__/'],
watchPathIgnorePatterns: ['/node_modules/', '/__fixtures__/'],
coverageDirectory: '../../coverage/tools/workspace-plugin',
setupFiles: ['<rootDir>/jest-setup.js'],
} as import('@jest/types').Config.InitialOptions;
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"presets": [
[
"@griffel",
{
"modules": [
{ "moduleSource": "@griffel/core", "importName": "makeStyles" },
{ "moduleSource": "@griffel/react", "importName": "makeStyles" }
]
}
]
],
"plugins": ["annotate-pure-calls", "@babel/transform-react-pure-annotations"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"exclude": [
"/testing",
"/**/*.cy.ts",
"/**/*.cy.tsx",
"/**/*.spec.ts",
"/**/*.spec.tsx",
"/**/*.test.ts",
"/**/*.test.tsx"
],
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": false,
"dynamicImport": false
},
"externalHelpers": true,
"transform": {
"react": {
"runtime": "classic",
"useSpread": true
}
},
"target": "es2019"
},
"minify": false,
"sourceMaps": true
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { greeter } from './greeter';

describe('greeter', () => {
it('should work', () => {
expect(greeter('Hello', { name: 'Mr Wick' })).toContain('Hello');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { makeStyles } from '@griffel/react';

export const useStyles = makeStyles({
root: { color: 'red' },
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useStyles } from './greeter.styles';
export function greeter(greeting: string, user: User): string {
const styles = useStyles();
return `<h1 class="${styles}">${greeting} ${user.name} from ${user.hometown?.name}</h1>`;
}

type User = {
name: string;
hometown?: {
name: string;
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { greeter } from './greeter';
213 changes: 209 additions & 4 deletions tools/workspace-plugin/src/executors/build/executor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,223 @@
import { ExecutorContext } from '@nx/devkit';
import { type ExecutorContext, logger, stripIndents } from '@nx/devkit';

import { BuildExecutorSchema } from './schema';
import executor from './executor';
import { join } from 'node:path';
import { existsSync, readFileSync, readdirSync } from 'node:fs';

// ===== mocks start =====
import { rm } from 'node:fs/promises';
import { measureStart, measureEnd } from '../../utils';
// ===== mocks end =====

const options: BuildExecutorSchema = {
sourceRoot: 'src',
outputPathRoot: 'libs/proj/dist',
moduleOutput: [
{
module: 'commonjs',
outputPath: 'lib-commonjs',
},
{
module: 'es6',
outputPath: 'lib',
},
],
assets: [
{
input: 'libs/proj/',
output: 'assets/',
glob: '*.md',
},
],
clean: true,
};

const workspaceRoot = join(__dirname, '__fixtures__/executor');

const options: BuildExecutorSchema = {};
const context: ExecutorContext = {
root: '',
root: workspaceRoot,
cwd: process.cwd(),
isVerbose: false,
projectName: 'proj',
projectsConfigurations: {
version: 2,
projects: {
proj: {
root: 'libs/proj',
name: 'proj',
},
},
},
};

jest.mock('node:fs/promises', () => {
const actualFs = jest.requireActual('node:fs/promises');

return {
...actualFs,
rm: jest.fn(actualFs.rm),
};
});

jest.mock('../../utils', () => {
return {
measureStart: jest.fn(),
measureEnd: jest.fn(),
};
});

const rmMock = rm as jest.Mock;
const measureStartMock = measureStart as jest.Mock;
const measureEndMock = measureEnd as jest.Mock;

describe('Build Executor', () => {
it('can run', async () => {
const loggerLogSpy = jest.spyOn(logger, 'log').mockImplementation(() => {
return;
});
const loggerVerboseSpy = jest.spyOn(logger, 'verbose').mockImplementation(() => {
return;
});

const output = await executor(options, context);
expect(output.success).toBe(true);
});

const loggerLogSpyCalls = loggerLogSpy.mock.calls.flat();
const [clearLogs, ...restOfLogs] = loggerLogSpyCalls;

expect(stripIndents`${clearLogs}`).toEqual(stripIndents`
Cleaning outputs:
- ${workspaceRoot}/libs/proj/lib-commonjs
- ${workspaceRoot}/libs/proj/lib
- ${workspaceRoot}/libs/proj/dist/assets/spec.md
`);

expect(restOfLogs).toEqual([
'Compiling with SWC for module:es6...',
'Processing griffel AOT with babel: 1 files',
'Compiling with SWC for module:commonjs...',
]);

expect(loggerVerboseSpy.mock.calls.flat()).toEqual([
`babel: transformed ${workspaceRoot}/libs/proj/lib/greeter.styles.js`,
]);

expect(rmMock.mock.calls.flat()).toEqual([
`${workspaceRoot}/libs/proj/lib-commonjs`,
{
force: true,
recursive: true,
},
`${workspaceRoot}/libs/proj/lib`,
{
force: true,
recursive: true,
},
`${workspaceRoot}/libs/proj/dist/assets/spec.md`,
{
force: true,
recursive: true,
},
]);

expect(measureStartMock).toHaveBeenCalledTimes(1);
expect(measureEndMock).toHaveBeenCalledTimes(1);

// =====================
// assert build Assets
// =====================
expect(existsSync(join(workspaceRoot, 'libs/proj/dist/assets', 'spec.md'))).toBe(true);
expect(readdirSync(join(workspaceRoot, 'libs/proj/lib'))).toEqual([
'greeter.js',
'greeter.js.map',
'greeter.styles.js',
'greeter.styles.js.map',
'index.js',
'index.js.map',
]);
expect(readdirSync(join(workspaceRoot, 'libs/proj/lib-commonjs'))).toEqual([
'greeter.js',
'greeter.js.map',
'greeter.styles.js',
'greeter.styles.js.map',
'index.js',
'index.js.map',
]);

// ====================================
// assert swc output based on settings
// ====================================
expect(readFileSync(join(workspaceRoot, 'libs/proj/lib/greeter.js'), 'utf-8')).toMatchInlineSnapshot(`
"import { useStyles } from './greeter.styles';
export function greeter(greeting, user) {
var _user_hometown;
const styles = useStyles();
return \`<h1 class=\\"\${styles}\\">\${greeting} \${user.name} from \${(_user_hometown = user.hometown) === null || _user_hometown === void 0 ? void 0 : _user_hometown.name}</h1>\`;
}
"
`);
expect(readFileSync(join(workspaceRoot, 'libs/proj/lib/greeter.js.map'), 'utf-8')).toMatchInlineSnapshot(
`"{\\"version\\":3,\\"sources\\":[\\"greeter.ts\\"],\\"sourcesContent\\":[\\"import { useStyles } from './greeter.styles';\\\\nexport function greeter(greeting: string, user: User): string {\\\\n const styles = useStyles();\\\\n return \`<h1 class=\\\\\\"\${styles}\\\\\\">\${greeting} \${user.name} from \${user.hometown?.name}</h1>\`;\\\\n}\\\\n\\\\ntype User = {\\\\n name: string;\\\\n hometown?: {\\\\n name: string;\\\\n };\\\\n};\\\\n\\"],\\"names\\":[\\"useStyles\\",\\"greeter\\",\\"greeting\\",\\"user\\",\\"styles\\",\\"name\\",\\"hometown\\"],\\"rangeMappings\\":\\";;;;;\\",\\"mappings\\":\\"AAAA,SAASA,SAAS,QAAQ,mBAAmB;AAC7C,OAAO,SAASC,QAAQC,QAAgB,EAAEC,IAAU;QAEYA;IAD9D,MAAMC,SAASJ;IACf,OAAO,CAAC,WAAW,EAAEI,OAAO,EAAE,EAAEF,SAAS,CAAC,EAAEC,KAAKE,IAAI,CAAC,MAAM,GAAEF,iBAAAA,KAAKG,QAAQ,cAAbH,qCAAAA,eAAeE,IAAI,CAAC,KAAK,CAAC;AAC1F\\"}"`,
);

expect(readFileSync(join(workspaceRoot, 'libs/proj/lib-commonjs/greeter.js'), 'utf-8')).toMatchInlineSnapshot(`
"\\"use strict\\";
Object.defineProperty(exports, \\"__esModule\\", {
value: true
});
Object.defineProperty(exports, \\"greeter\\", {
enumerable: true,
get: function() {
return greeter;
}
});
const _greeterstyles = require(\\"./greeter.styles\\");
function greeter(greeting, user) {
var _user_hometown;
const styles = (0, _greeterstyles.useStyles)();
return \`<h1 class=\\"\${styles}\\">\${greeting} \${user.name} from \${(_user_hometown = user.hometown) === null || _user_hometown === void 0 ? void 0 : _user_hometown.name}</h1>\`;
}
"
`);

// =====================
// assert griffel AOT
// =====================
expect(readFileSync(join(workspaceRoot, 'libs/proj/lib/greeter.styles.js'), 'utf-8')).toMatchInlineSnapshot(`
"import { __styles } from '@griffel/react';
export const useStyles = /*#__PURE__*/__styles({
root: {
sj55zd: \\"fe3e8s9\\"
}
}, {
d: [\\".fe3e8s9{color:red;}\\"]
});"
`);
expect(readFileSync(join(workspaceRoot, 'libs/proj/lib-commonjs/greeter.styles.js'), 'utf-8'))
.toMatchInlineSnapshot(`
"\\"use strict\\";
Object.defineProperty(exports, \\"__esModule\\", {
value: true
});
Object.defineProperty(exports, \\"useStyles\\", {
enumerable: true,
get: function() {
return useStyles;
}
});
const _react = require(\\"@griffel/react\\");
const useStyles = /*#__PURE__*/ (0, _react.__styles)({
root: {
sj55zd: \\"fe3e8s9\\"
}
}, {
d: [
\\".fe3e8s9{color:red;}\\"
]
});
"
`);
}, 30000);
});
51 changes: 43 additions & 8 deletions tools/workspace-plugin/src/executors/build/executor.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,46 @@
import { PromiseExecutor } from '@nx/devkit';
import { BuildExecutorSchema } from './schema';

const runExecutor: PromiseExecutor<BuildExecutorSchema> = async options => {
console.log('Executor ran for Build', options);
return {
success: true,
};
import { type ExecutorContext, type PromiseExecutor } from '@nx/devkit';

import { compileSwc } from './lib/swc';
import { compileWithGriffelStylesAOT, hasStylesFilesToProcess } from './lib/babel';
import { assetGlobsToFiles, copyAssets } from './lib/assets';
import { cleanOutput } from './lib/clean';
import { NormalizedOptions, normalizeOptions, processAsyncQueue } from './lib/shared';

import { measureEnd, measureStart } from '../../utils';

import { type BuildExecutorSchema } from './schema';

const runExecutor: PromiseExecutor<BuildExecutorSchema> = async (schema, context) => {
measureStart('BuildExecutor');

const options = normalizeOptions(schema, context);

const success = await runBuild(options, context);

measureEnd('BuildExecutor');

return { success };
};

export default runExecutor;

// ===========

async function runBuild(options: NormalizedOptions, context: ExecutorContext): Promise<boolean> {
const assetFiles = assetGlobsToFiles(options.assets ?? [], context.root, options.outputPathRoot);

const cleanResult = await cleanOutput(options, assetFiles);
if (!cleanResult) {
return false;
}

if (hasStylesFilesToProcess(options)) {
return compileWithGriffelStylesAOT(options, () => copyAssets(assetFiles));
}

const compilationQueue = options.moduleOutput.map(outputConfig => {
return compileSwc(outputConfig, options);
});

return processAsyncQueue(compilationQueue, () => copyAssets(assetFiles));
}
Loading

0 comments on commit 20bfbad

Please sign in to comment.