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: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:

- name: build, test, lint, test-ssr (affected)
run: |
FLUENT_JEST_WORKER=2 yarn nx affected -t build test lint type-check test-ssr test-integration verify-packaging --nxBail
FLUENT_JEST_WORKER=2 yarn nx affected -t build test lint type-check test-ssr test-integration verify-packaging verify-bundle-isolation --nxBail

- name: 'Check for unstaged changes'
run: |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "chore: verify headless entry points do not bundle tabster, Griffel or react-icons",
"packageName": "@fluentui/react-headless-components-preview",
"email": "martinhochel@microsoft.com",
"dependentChangeType": "none"
}
10 changes: 6 additions & 4 deletions nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@
"cache": true
},
"e2e": {
"dependsOn": [],
"dependsOn": [
{
"target": "build",
"projects": "scripts-cypress"
}
],
"cache": true,
"inputs": ["default", "{projectRoot}/cypress.config.ts", "!{projectRoot}/**/?(*.)+cy.[jt]s?(x)?"]
},
Expand Down Expand Up @@ -163,16 +168,13 @@
"apps/public-docsite-v9-headless/**",
"apps/rit-tests-v9/*",
"apps/rit-tests-v8/*",

"tools/**/*",
"scripts/**/*",
"packages/eslint-plugin/**",

"packages/tokens/**",
"packages/react-conformance/**",
"packages/react-components/**/*",
"packages/charts/react-charts/**/*",

"packages/foundation-legacy/**",
"packages/jest-serializer-merge-styles/**",
"packages/react/**",
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@fluentui/react-integration-tester": "*",
"@fluentui/scripts-test-ssr": "*",
"@fluentui/storybook-llms-extractor": "*",
"@fluentui/verify-bundle-isolation": "*",
"@griffel/babel-preset": "1.5.8",
"@griffel/eslint-plugin": "^2.0.0",
"@griffel/jest-serializer": "1.1.24",
Expand Down Expand Up @@ -381,6 +382,7 @@
"swc-loader": "0.2.6",
"syncpack/minimatch": "^9.0.7",
"tar-fs": "2.1.4",
"webpack": "5.108.4",
"ws": "^8.21.1"
},
"nx": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "../../../../tools/verify-bundle-isolation/schema.json",
"fixturesRoot": "./bundle-size",
"externals": ["react", "react-dom", "react/jsx-runtime", "react/compiler-runtime"],
"forbiddenPackages": ["tabster", "@griffel/*", "@fluentui/react-icons"],
"allowedViolations": {
"AllComponents.fixture.js": ["@griffel/core", "@griffel/react"],
"TagPicker.fixture.js": ["@griffel/core", "@griffel/react"]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,14 @@

const fluentPlugin = require('@fluentui/eslint-plugin');

module.exports = [...fluentPlugin.configs['flat/react']];
module.exports = [
...fluentPlugin.configs['flat/react'],
{
// Build-time verification tooling - not shipped, runs on Node, reports via stdout.
files: ['scripts/**/*.js'],
rules: {
'no-console': 'off',
'import/no-extraneous-dependencies': 'off',
},
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@
},
"devDependencies": {
"@fluentui/scripts-cypress": "*",
"@fluentui/verify-bundle-isolation": "*",
"@oddbird/popover-polyfill": "^0.6.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@
"options": {
"exportSubpaths": true
}
},
"verify-bundle-isolation": {
"cache": true,
"dependsOn": ["build", "^build"],
"command": "yarn run -T verify-bundle-isolation",
"options": {
"cwd": "{projectRoot}"
},
"inputs": ["default", "^default", { "externalDependencies": ["ajv", "webpack"] }],
"outputs": ["{projectRoot}/dist/bundle-isolation"],
"metadata": {
"technologies": ["webpack"],
"description": "Assert entry points do not bundle tabster, Griffel or react-icons"
}
}
}
}
17 changes: 17 additions & 0 deletions scripts/cypress/.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"exclude": ["/**/*.cy.ts", "/**/*.cy.tsx", "/**/*.spec.ts", "/**/*.spec.tsx", "/**/*.test.ts", "/**/*.test.tsx"],
"jsc": {
"baseUrl": ".",
"parser": {
"syntax": "typescript",
"tsx": false,
"decorators": false,
"dynamicImport": false
},
"externalHelpers": false,
"target": "es2022"
},
"minify": false,
"sourceMaps": true
}
11 changes: 11 additions & 0 deletions scripts/cypress/config/api-extractor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"extends": "@fluentui/scripts-api-extractor/api-extractor.common.v-next.json",
"apiReport": {
"enabled": false
},
"compiler": {
"skipLibCheck": true
},
"mainEntryPointFilePath": "<projectRoot>/../../../../dist/out-tsc/types/index.d.ts"
}
File renamed without changes.
File renamed without changes.
23 changes: 21 additions & 2 deletions scripts/cypress/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
{
"name": "@fluentui/scripts-cypress",
"type": "module",
"version": "0.0.1",
"private": true,
"main": "src/index.ts",
"browser": "src/browser/index.ts"
"main": "lib-commonjs/index.cjs",
"module": "lib/index.js",
"browser": "lib/browser/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"browser": "./lib/browser/index.js",
"import": "./lib/index.js",
"require": "./lib-commonjs/index.cjs"
},
"./package.json": "./package.json"
},
"files": [
"lib",
"lib-commonjs",
"dist/*.d.ts",
"support",
"fixtures"
]
}
17 changes: 16 additions & 1 deletion scripts/cypress/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,20 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "scripts/cypress/src",
"projectType": "library",
"tags": ["tools"]
"tags": ["tools"],
"targets": {
"build": {
"executor": "@fluentui/workspace-plugin:build",
"outputs": ["{projectRoot}/lib", "{projectRoot}/lib-commonjs"],
"options": {
"sourceRoot": "src",
"outputPathRoot": "{projectRoot}",
"moduleOutput": [
{ "module": "es6", "outputPath": "lib" },
{ "module": "commonjs", "outputPath": "lib-commonjs" }
],
"generateApi": true
}
}
}
}
10 changes: 7 additions & 3 deletions scripts/cypress/src/base.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const cypressWebpackConfig = (): Configuration => {
baseWebpackConfig.resolve.plugins ??= [];
baseWebpackConfig.resolve.plugins.push(
new TsconfigPathsPlugin({
configFile: path.resolve(__dirname, '../../../tsconfig.base.json'),
configFile: path.resolve(import.meta.dirname, '../../../tsconfig.base.json'),
}),
);

Expand All @@ -82,8 +82,12 @@ interface BaseConfig extends Cypress.ConfigOptions {
* This is a workaround for the issue where Cypress does not resolve the paths correctly, as it
* internally prepend the __dirname, making them invalid
*
* Heads up! `import.meta.dirname` is compiled by SWC to `__dirname` for the CommonJS (`lib-commonjs`)
* output and kept as-is for the ESM (`lib`) output. Both live one level under the package root, so
* `../support` and `../fixtures` resolve to the package's `support/` and `fixtures/` source folders
* (which Cypress' webpack bundles for the browser).
*/
const sharedConfigSupportRootDir = path.join(__dirname, './support');
const sharedConfigSupportRootDir = path.join(import.meta.dirname, '../support');
const projectSupportDir = path.relative(projectRoot, sharedConfigSupportRootDir);

export const baseConfig = defineConfig({
Expand All @@ -106,7 +110,7 @@ export const baseConfig = defineConfig({
// Screenshots go under <pkg>/cypress/screenshots and can be useful to look at after failures in
// local headless runs (especially if the failure is specific to headless runs)
// screenshotOnRunFailure: isLocalRun && argv.mode === 'run',
fixturesFolder: path.join(__dirname, './fixtures'),
fixturesFolder: path.join(import.meta.dirname, '../fixtures'),
}) as BaseConfig;

/**
Expand Down
2 changes: 1 addition & 1 deletion scripts/cypress/src/browser/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Browser-only entry: expose just the custom mount used in Component Testing
export { mount } from './mount';
export { mount } from './mount.js';
22 changes: 19 additions & 3 deletions scripts/cypress/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
export { baseConfig, baseWebpackConfig } from './base.config';
/**
* `@fluentui/scripts-cypress` ships a real dual build (ESM under `lib/`, CommonJS under
* `lib-commonjs/*.cjs`, `type: module`). Consumers resolve the format that matches them:
* - `type: module` packages' `cypress.config.ts` → `import` condition → `lib/index.js` (ESM)
* - CommonJS configs (and the rit harness via ts-node `require`) → `require` condition → `lib-commonjs/index.cjs`
*
* Because it's built (not consumed as raw `.ts` source), per-file TS transpilers like ts-node never
* recompile this package against a consumer's tsconfig, so a CommonJS consumer no longer has to become
* `type: module` to use it. The `mount` browser API is served via the package `browser` field.
*/

import type { mount as cypressMount } from '@cypress/react';

export { baseConfig, baseWebpackConfig } from './base.config.js';

// =========== BROWSER APIs ==================

// TODO: Browser related APIs should be exposed via export maps or moved to separate package
// Expose Browser specific API under same barrel
export declare const mount: typeof import('./browser').mount;
// Expose Browser specific API under same barrel; resolved at runtime via the package `browser` field.
// The type is aliased from `@cypress/react` rather than `typeof import('./browser/index.js')` because
// api-extractor cannot roll up a relative dynamic import - it leaks into `dist/index.d.ts` as an
// unresolvable specifier, silently degrading `mount` to `any` for consumers.
export declare const mount: typeof cypressMount;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable no-undef */
import { realPress } from 'cypress-real-events/commands/realPress.js';
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
Expand Down Expand Up @@ -37,5 +38,5 @@ const CYPRESS_MAJOR_VERSION =
typeof Cypress !== 'undefined' && Cypress.version ? Number(String(Cypress.version).split('.')[0]) : undefined;

if (CYPRESS_MAJOR_VERSION === 13) {
Cypress.Commands.add('press', /** @type {any} */ (require('cypress-real-events/commands/realPress').realPress));
Cypress.Commands.add('press', /** @type {any} */ (realPress));
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
// ***********************************************************

// Import commands.js using ES2015 syntax:
import 'cypress-real-events/support';
import './commands';
import 'cypress-real-events/support.js';
import './commands.js';

// Alternatively you can use CommonJS syntax:
// require('./commands')
5 changes: 4 additions & 1 deletion scripts/cypress/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"lib": ["ES2019"],
"declaration": true,
"rootDir": "./src",
"declarationDir": "../../dist/out-tsc/types",
"outDir": "../../dist/out-tsc",
"lib": ["ES2019", "dom"],
"types": ["node", "cypress"]
},
"exclude": ["**/*.spec.ts", "**/*.test.ts"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Local folder barrel — same conflation risk as a package barrel.
export { useLocalLight } from './local-light';
export { useLocalHeavy } from './local-heavy';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { runHeavy } from 'heavy-runtime';

export type LocalHeavyType = { tag: 'local-heavy' };

export function useLocalHeavy(): { tag: 'heavy' } {
return runHeavy();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { runLight } from 'light-helper';

export type LocalLightOptions = { mode: 'light' };

export function useLocalLight(opts?: LocalLightOptions): void {
runLight(opts);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useLocalHeavy } from './local-heavy';

export function useLocalTrigger(): { tag: 'heavy' } {
return useLocalHeavy();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { runLight } from 'light-helper';

export function useCleanExport(): void {
runLight();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { runHeavy } from 'heavy-runtime';

export function useDirtyExport(): { tag: 'heavy' } {
return runHeavy();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Barrel exposing one clean and one forbidden-runtime-dependent export. Importing the clean one
// must not inherit the dependencies of its sibling.
export { useCleanExport } from './clean';
export { useDirtyExport } from './dirty';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Widget } from './widget';
export type { WidgetHostProps, WidgetSlots } from './types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Widget } from './widget';

// `typeof Widget` extracts the component's type; it does not consume its runtime.
export type WidgetSlots = { widget: typeof Widget };

export type WidgetHostProps = { slots: WidgetSlots };
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { runHeavy } from 'heavy-runtime';

export type WidgetProps = { tag: 'widget' };

// A component whose implementation is dirty but whose type surface is not.
export const Widget = (props: WidgetProps): { tag: 'heavy' } => {
return runHeavy();
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function runHeavySub(): { tag: 'heavy-sub' } {
return { tag: 'heavy-sub' };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useBenign } from 'wrapper-pkg';

// Mirrors `useActiveDescendant`: an intermediate package that consumes only the benign export of
// the wrapper package.
export function useRelay(): void {
useBenign();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { HeavyOptions } from 'heavy-runtime';

// Stands in for a package linted against built output rather than source.
export declare type DistHeavy = { tag: 'dist-heavy'; options: HeavyOptions };

export declare type DistClean = { tag: 'dist-clean' };

export declare function useDistClean(): DistClean;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { runHeavy } from 'heavy-runtime';

export function useUnlisted(): { tag: 'heavy' } {
return runHeavy();
}
Loading
Loading