@samitouri / QOS-React-2 / commits / d364d8555f

Set up experimental builds (#17071)

* Don't bother including `unstable_` in error The method names don't get stripped out of the production bundles because they are passed as arguments to the error decoder. Let's just always use the unprefixed APIs in the messages. * Set up experimental builds The experimental builds are packaged exactly like builds in the stable release channel: same file structure, entry points, and npm package names. The goal is to match what will eventually be released in stable as closely as possible, but with additional features turned on. Versioning and Releasing ------------------------ The experimental builds will be published to the same registry and package names as the stable ones. However, they will be versioned using a separate scheme. Instead of semver versions, experimental releases will receive arbitrary version strings based on their content hashes. The motivation is to thwart attempts to use a version range to match against future experimental releases. The only way to install or depend on an experimental release is to refer to the specific version number. Building -------- I did not use the existing feature flag infra to configure the experimental builds. The reason is because feature flags are designed to configure a single package. They're not designed to generate multiple forks of the same package; for each set of feature flags, you must create a separate package configuration. Instead, I've added a new build dimension called the **release channel**. By default, builds use the **stable** channel. There's also an **experimental** release channel. We have the option to add more in the future. There are now two dimensions per artifact: build type (production, development, or profiling), and release channel (stable or experimental). These are separate dimensions because they are combinatorial: there are stable and experimental production builds, stable and experimental developmenet builds, and so on. You can add something to an experimental build by gating on `__EXPERIMENTAL__`, similar to how we use `__DEV__`. Anything inside these branches will be excluded from the stable builds. This gives us a low effort way to add experimental behavior in any package without setting up feature flags or configuring a new package.

Andrew Clark committed Oct 14, 2019 at 10:46 UTC d364d8555f0c2c44e8fb624068b9fff30d5908ae
11 files changed +126 -47
.circleci/config.yml
+94 -15
@@ -22,6 +22,32 @@ aliases:
22 - &attach_workspace
23 at: build
24
25 + - &process_artifacts
26 + docker: *docker
27 + environment: *environment
28 + steps:
29 + - checkout
30 + - attach_workspace: *attach_workspace
31 + - *restore_yarn_cache
32 + - *run_yarn
33 + - run: node ./scripts/rollup/consolidateBundleSizes.js
34 + - run: ./scripts/circleci/upload_build.sh
35 + - run: ./scripts/circleci/pack_and_store_artifact.sh
36 + - store_artifacts:
37 + path: ./node_modules.tgz
38 + - store_artifacts:
39 + path: ./build.tgz
40 + - store_artifacts:
41 + path: ./build/bundle-sizes.json
42 + - store_artifacts:
43 + # TODO: Update release script to use local file instead of pulling
44 + # from artifacts.
45 + path: ./scripts/error-codes/codes.json
46 + - persist_to_workspace:
47 + root: build
48 + paths:
49 + - bundle-sizes.json
50 +
51 jobs:
52 setup:
53 docker: *docker
@@ -74,6 +100,18 @@ jobs:
100 - *run_yarn
101 - run: yarn test --maxWorkers=2
102
103 + test_source_experimental:
104 + docker: *docker
105 + environment: *environment
106 + steps:
107 + - checkout
108 + - *restore_yarn_cache
109 + - *run_yarn
110 + - run:
111 + environment:
112 + RELEASE_CHANNEL: experimental
113 + command: yarn test --maxWorkers=2
114 +
115 test_source_persistent:
116 docker: *docker
117 environment: *environment
@@ -105,16 +143,49 @@ jobs:
143 - run: ./scripts/circleci/add_build_info_json.sh
144 - run: ./scripts/circleci/update_package_versions.sh
145 - run: yarn build
146 + - run: echo "stable" >> build/RELEASE_CHANNEL
147 + - persist_to_workspace:
148 + root: build
149 + paths:
150 + - RELEASE_CHANNEL
151 + - facebook-www
152 + - node_modules
153 + - react-native
154 + - dist
155 + - sizes/*.json
156 +
157 + build_experimental:
158 + docker: *docker
159 + environment: *environment
160 + parallelism: 20
161 + steps:
162 + - checkout
163 + - *restore_yarn_cache
164 + - *run_yarn
165 + - run:
166 + environment:
167 + RELEASE_CHANNEL: experimental
168 + command: |
169 + ./scripts/circleci/add_build_info_json.sh
170 + ./scripts/circleci/update_package_versions.sh
171 + yarn build
172 + - run: echo "experimental" >> build/RELEASE_CHANNEL
173 - persist_to_workspace:
174 root: build
175 paths:
176 + - RELEASE_CHANNEL
177 - facebook-www
178 - node_modules
179 - react-native
180 - dist
181 - sizes/*.json
182
117 - process_artifacts:
183 + # These jobs are named differently so we can distinguish the stable and
184 + # and experimental artifacts
185 + process_artifacts: *process_artifacts
186 + process_artifacts_experimental: *process_artifacts
187 +
188 + sizebot:
189 docker: *docker
190 environment: *environment
191 steps:
@@ -122,20 +193,10 @@ jobs:
193 - attach_workspace: *attach_workspace
194 - *restore_yarn_cache
195 - *run_yarn
196 + # This runs in the process_artifacts job, too, but it's faster to run
197 + # this step in both jobs instead of running the jobs sequentially
198 - run: node ./scripts/rollup/consolidateBundleSizes.js
199 - run: node ./scripts/tasks/danger
127 - - run: ./scripts/circleci/upload_build.sh
128 - - run: ./scripts/circleci/pack_and_store_artifact.sh
129 - - store_artifacts:
130 - path: ./node_modules.tgz
131 - - store_artifacts:
132 - path: ./build.tgz
133 - - store_artifacts:
134 - path: ./build/bundle-sizes.json
135 - - store_artifacts:
136 - # TODO: Update release script to use local file instead of pulling
137 - # from artifacts.
138 - path: ./scripts/error-codes/codes.json
200
201 lint_build:
202 docker: *docker
@@ -208,7 +269,7 @@ jobs:
269
270 workflows:
271 version: 2
211 - commit:
272 + stable:
273 jobs:
274 - setup
275 - lint:
@@ -232,6 +293,9 @@ workflows:
293 - process_artifacts:
294 requires:
295 - build
296 + - sizebot:
297 + requires:
298 + - build
299 - lint_build:
300 requires:
301 - build
@@ -247,9 +311,24 @@ workflows:
311 - test_dom_fixtures:
312 requires:
313 - build
250 - hourly:
314 +
315 + experimental:
316 + jobs:
317 + - setup
318 + - test_source_experimental:
319 + requires:
320 + - setup
321 + - build_experimental:
322 + requires:
323 + - setup
324 + - process_artifacts_experimental:
325 + requires:
326 + - build_experimental
327 +
328 + fuzz_tests:
329 triggers:
330 - schedule:
331 + # Fuzz tests run hourly
332 cron: "0 * * * *"
333 filters:
334 branches:
.eslintrc.js
+1
@@ -149,6 +149,7 @@ module.exports = {
149 spyOnProd: true,
150 __PROFILE__: true,
151 __UMD__: true,
152 + __EXPERIMENTAL__: true,
153 trustedTypes: true,
154 },
155 };
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+5 -7
@@ -103,9 +103,7 @@ describe('ReactDOMRoot', () => {
103 it('throws a good message on invalid containers', () => {
104 expect(() => {
105 ReactDOM.unstable_createRoot(<div>Hi</div>);
106 - }).toThrow(
107 - 'unstable_createRoot(...): Target container is not a DOM element.',
108 - );
106 + }).toThrow('createRoot(...): Target container is not a DOM element.');
107 });
108
109 it('warns when rendering with legacy API into createRoot() container', () => {
@@ -119,7 +117,7 @@ describe('ReactDOMRoot', () => {
117 [
118 // We care about this warning:
119 'You are calling ReactDOM.render() on a container that was previously ' +
122 - 'passed to ReactDOM.unstable_createRoot(). This is not supported. ' +
120 + 'passed to ReactDOM.createRoot(). This is not supported. ' +
121 'Did you mean to call root.render(element)?',
122 // This is more of a symptom but restructuring the code to avoid it isn't worth it:
123 'Replacing React-rendered children with a new root component.',
@@ -142,7 +140,7 @@ describe('ReactDOMRoot', () => {
140 [
141 // We care about this warning:
142 'You are calling ReactDOM.hydrate() on a container that was previously ' +
145 - 'passed to ReactDOM.unstable_createRoot(). This is not supported. ' +
143 + 'passed to ReactDOM.createRoot(). This is not supported. ' +
144 'Did you mean to call createRoot(container, {hydrate: true}).render(element)?',
145 // This is more of a symptom but restructuring the code to avoid it isn't worth it:
146 'Replacing React-rendered children with a new root component.',
@@ -163,7 +161,7 @@ describe('ReactDOMRoot', () => {
161 [
162 // We care about this warning:
163 'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
166 - 'passed to ReactDOM.unstable_createRoot(). This is not supported. Did you mean to call root.unmount()?',
164 + 'passed to ReactDOM.createRoot(). This is not supported. Did you mean to call root.unmount()?',
165 // This is more of a symptom but restructuring the code to avoid it isn't worth it:
166 "The node you're attempting to unmount was rendered by React and is not a top-level container.",
167 ],
@@ -202,7 +200,7 @@ describe('ReactDOMRoot', () => {
200 expect(() => {
201 ReactDOM.unstable_createRoot(container);
202 }).toWarnDev(
205 - 'You are calling ReactDOM.unstable_createRoot() on a container that was previously ' +
203 + 'You are calling ReactDOM.createRoot() on a container that was previously ' +
204 'passed to ReactDOM.render(). This is not supported.',
205 {withoutStack: true},
206 );
packages/react-dom/src/client/ReactDOM.js
+6 -18
@@ -451,9 +451,8 @@ const ReactDOM: Object = {
451 warningWithoutStack(
452 !container._reactHasBeenPassedToCreateRootDEV,
453 'You are calling ReactDOM.hydrate() on a container that was previously ' +
454 - 'passed to ReactDOM.%s(). This is not supported. ' +
454 + 'passed to ReactDOM.createRoot(). This is not supported. ' +
455 'Did you mean to call createRoot(container, {hydrate: true}).render(element)?',
456 - enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot',
456 );
457 }
458 // TODO: throw or warn if we couldn't hydrate?
@@ -479,9 +478,8 @@ const ReactDOM: Object = {
478 warningWithoutStack(
479 !container._reactHasBeenPassedToCreateRootDEV,
480 'You are calling ReactDOM.render() on a container that was previously ' +
482 - 'passed to ReactDOM.%s(). This is not supported. ' +
481 + 'passed to ReactDOM.createRoot(). This is not supported. ' +
482 'Did you mean to call root.render(element)?',
484 - enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot',
483 );
484 }
485 return legacyRenderSubtreeIntoContainer(
@@ -526,8 +524,7 @@ const ReactDOM: Object = {
524 warningWithoutStack(
525 !container._reactHasBeenPassedToCreateRootDEV,
526 'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
529 - 'passed to ReactDOM.%s(). This is not supported. Did you mean to call root.unmount()?',
530 - enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot',
527 + 'passed to ReactDOM.createRoot(). This is not supported. Did you mean to call root.unmount()?',
528 );
529 }
530
@@ -650,13 +647,9 @@ function createRoot(
647 container: DOMContainer,
648 options?: RootOptions,
649 ): _ReactRoot {
653 - const functionName = enableStableConcurrentModeAPIs
654 - ? 'createRoot'
655 - : 'unstable_createRoot';
650 invariant(
651 isValidContainer(container),
658 - '%s(...): Target container is not a DOM element.',
659 - functionName,
652 + 'createRoot(...): Target container is not a DOM element.',
653 );
654 warnIfReactDOMContainerInDEV(container);
655 return new ReactRoot(container, options);
@@ -666,13 +659,9 @@ function createSyncRoot(
659 container: DOMContainer,
660 options?: RootOptions,
661 ): _ReactRoot {
669 - const functionName = enableStableConcurrentModeAPIs
670 - ? 'createRoot'
671 - : 'unstable_createRoot';
662 invariant(
663 isValidContainer(container),
674 - '%s(...): Target container is not a DOM element.',
675 - functionName,
664 + 'createRoot(...): Target container is not a DOM element.',
665 );
666 warnIfReactDOMContainerInDEV(container);
667 return new ReactSyncRoot(container, BatchedRoot, options);
@@ -682,9 +671,8 @@ function warnIfReactDOMContainerInDEV(container) {
671 if (__DEV__) {
672 warningWithoutStack(
673 !container._reactRootContainer,
685 - 'You are calling ReactDOM.%s() on a container that was previously ' +
674 + 'You are calling ReactDOM.createRoot() on a container that was previously ' +
675 'passed to ReactDOM.render(). This is not supported.',
687 - enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot',
676 );
677 container._reactHasBeenPassedToCreateRootDEV = true;
678 }
packages/shared/ReactFeatureFlags.js
+1 -1
@@ -52,7 +52,7 @@ export const disableInputAttributeSyncing = false;
52
53 // These APIs will no longer be "unstable" in the upcoming 16.7 release,
54 // Control this behavior with a flag to support 16.6 minor releases in the meanwhile.
55 -export const enableStableConcurrentModeAPIs = false;
55 +export const enableStableConcurrentModeAPIs = __EXPERIMENTAL__;
56
57 export const warnAboutShorthandPropertyCollision = false;
58
scripts/error-codes/codes.json
+1 -1
@@ -297,7 +297,7 @@
297 "296": "Log of yielded values is not empty. Call expect(ReactTestRenderer).unstable_toHaveYielded(...) first.",
298 "297": "The matcher `unstable_toHaveYielded` expects an instance of React Test Renderer.\n\nTry: expect(ReactTestRenderer).unstable_toHaveYielded(expectedYields)",
299 "298": "Hooks can only be called inside the body of a function component.",
300 - "299": "%s(...): Target container is not a DOM element.",
300 + "299": "createRoot(...): Target container is not a DOM element.",
301 "300": "Rendered fewer hooks than expected. This may be caused by an accidental early return statement.",
302 "301": "Too many re-renders. React limits the number of renders to prevent an infinite loop.",
303 "302": "It is not supported to run the profiling version of a renderer (for example, `react-dom/profiling`) without also replacing the `scheduler/tracing` module with `scheduler/tracing-profiling`. Your bundler might have a setting for aliasing both modules. Learn more at http://fb.me/react-profiling",
scripts/eslint-rules/__tests__/warning-and-invariant-args-test.internal.js
+1 -1
@@ -20,7 +20,7 @@ ruleTester.run('eslint-rules/warning-and-invariant-args', rule, {
20 'arbitraryFunction(a, b)',
21 // These messages are in the error code map
22 "invariant(false, 'Do not override existing functions.')",
23 - "invariant(false, '%s(...): Target container is not a DOM element.', str)",
23 + "invariant(false, 'createRoot(...): Target container is not a DOM element.')",
24 ],
25 invalid: [
26 {
scripts/flow/environment.js
+1
@@ -11,6 +11,7 @@
11
12 declare var __PROFILE__: boolean;
13 declare var __UMD__: boolean;
14 +declare var __EXPERIMENTAL__: boolean;
15
16 declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: any; /*?{
17 inject: ?((stuff: Object) => void)
scripts/jest/setupEnvironment.js
+1
@@ -7,6 +7,7 @@ if (NODE_ENV !== 'development' && NODE_ENV !== 'production') {
7 global.__DEV__ = NODE_ENV === 'development';
8 global.__PROFILE__ = NODE_ENV === 'development';
9 global.__UMD__ = false;
10 +global.__EXPERIMENTAL__ = process.env.RELEASE_CHANNEL === 'experimental';
11
12 if (typeof window !== 'undefined') {
13 global.requestIdleCallback = function(callback) {
scripts/release/utils.js
+8 -2
@@ -78,12 +78,16 @@ const getArtifactsList = async buildID => {
78 const getBuildInfo = async () => {
79 const cwd = join(__dirname, '..', '..');
80
81 + const isExperimental = process.env.RELEASE_CHANNEL === 'experimental';
82 +
83 const branch = await execRead('git branch | grep \\* | cut -d " " -f2', {
84 cwd,
85 });
86 const commit = await execRead('git show -s --format=%h', {cwd});
87 const checksum = await getChecksumForCurrentRevision(cwd);
86 - const version = `0.0.0-${commit}`;
88 + const version = isExperimental
89 + ? `0.0.0-experimental-${commit}`
90 + : `0.0.0-${commit}`;
91
92 // Only available for Circle CI builds.
93 // https://circleci.com/docs/2.0/env-vars/
@@ -94,7 +98,9 @@ const getBuildInfo = async () => {
98 const packageJSON = await readJson(
99 join(cwd, 'packages', 'react', 'package.json')
100 );
97 - const reactVersion = `${packageJSON.version}-canary-${commit}`;
101 + const reactVersion = isExperimental
102 + ? `${packageJSON.version}-experimental-canary-${commit}`
103 + : `${packageJSON.version}-canary-${commit}`;
104
105 return {branch, buildNumber, checksum, commit, reactVersion, version};
106 };
scripts/rollup/build.js
+7 -2
@@ -304,7 +304,8 @@ function getPlugins(
304 bundleType,
305 globalName,
306 moduleType,
307 - pureExternalModules
307 + pureExternalModules,
308 + isExperimentalBuild
309 ) {
310 const findAndRecordErrorCodes = extractErrorCodes(errorCodeOpts);
311 const forks = Modules.getForks(bundleType, entry, moduleType);
@@ -362,6 +363,7 @@ function getPlugins(
363 __PROFILE__: isProfiling || !isProduction ? 'true' : 'false',
364 __UMD__: isUMDBundle ? 'true' : 'false',
365 'process.env.NODE_ENV': isProduction ? "'production'" : "'development'",
366 + __EXPERIMENTAL__: isExperimentalBuild,
367 }),
368 // We still need CommonJS for external deps like object-assign.
369 commonjs(),
@@ -485,6 +487,8 @@ async function createBundle(bundle, bundleType) {
487 module => !importSideEffects[module]
488 );
489
490 + const isExperimentalBuild = process.env.RELEASE_CHANNEL === 'experimental';
491 +
492 const rollupConfig = {
493 input: resolvedEntry,
494 treeshake: {
@@ -508,7 +512,8 @@ async function createBundle(bundle, bundleType) {
512 bundleType,
513 bundle.global,
514 bundle.moduleType,
511 - pureExternalModules
515 + pureExternalModules,
516 + isExperimentalBuild
517 ),
518 // We can't use getters in www.
519 legacy: