@storybook/react-native 6.0.1-alpha.3 → 6.0.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/babel.config.js +3 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +1 -1
  4. package/dist/preview/Preview.d.ts +26 -22
  5. package/dist/preview/Preview.js +16 -21
  6. package/dist/preview/components/OnDeviceUI/OnDeviceUI.js +7 -4
  7. package/dist/preview/components/OnDeviceUI/animation.d.ts +4 -4
  8. package/dist/preview/components/OnDeviceUI/animation.js +21 -12
  9. package/package.json +31 -13
  10. package/scripts/__snapshots__/loader.test.js.snap +118 -0
  11. package/scripts/get-stories.js +3 -1
  12. package/scripts/handle-args.js +18 -0
  13. package/scripts/loader.js +80 -65
  14. package/scripts/loader.test.js +133 -0
  15. package/scripts/mocks/all-config-files/FakeComponent.tsx +1 -0
  16. package/scripts/mocks/all-config-files/FakeStory.stories.tsx +10 -0
  17. package/scripts/mocks/all-config-files/main.js +9 -0
  18. package/scripts/mocks/all-config-files/preview.js +24 -0
  19. package/scripts/mocks/blank-config/main.js +4 -0
  20. package/scripts/mocks/exclude-config-files/exclude-components/FakeComponent.tsx +1 -0
  21. package/scripts/mocks/exclude-config-files/exclude-components/FakeStory.stories.tsx +10 -0
  22. package/scripts/mocks/exclude-config-files/include-components/FakeComponent.tsx +1 -0
  23. package/scripts/mocks/exclude-config-files/include-components/FakeStory.stories.tsx +10 -0
  24. package/scripts/mocks/exclude-config-files/main.js +12 -0
  25. package/scripts/mocks/exclude-config-files/preview.js +24 -0
  26. package/scripts/mocks/no-preview/FakeComponent.tsx +1 -0
  27. package/scripts/mocks/no-preview/FakeStory.stories.tsx +10 -0
  28. package/scripts/mocks/no-preview/main.js +9 -0
  29. package/scripts/watcher.js +48 -15
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ presets: ['module:metro-react-native-babel-preset'],
3
+ };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  import { StoryApi } from '@storybook/addons';
4
4
  import { ClientApi } from '@storybook/client-api';
5
5
  import { ReactNode } from 'react';
6
+ import Preview from './preview';
7
+ export declare const preview: Preview;
6
8
  export declare const setAddon: ClientApi['setAddon'];
7
9
  export declare const addDecorator: ClientApi['addDecorator'];
8
10
  export declare const addParameters: ClientApi['addParameters'];
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import Preview from './preview';
2
- const preview = new Preview();
2
+ export const preview = new Preview();
3
3
  const rawStoriesOf = preview.api().storiesOf.bind(preview);
4
4
  export const setAddon = preview.api().setAddon.bind(preview);
5
5
  export const addDecorator = preview.api().addDecorator.bind(preview);
@@ -4,27 +4,32 @@ import Channel from '@storybook/channels';
4
4
  import { ClientApi, ConfigApi, StoryStore } from '@storybook/client-api';
5
5
  import { Loadable } from '@storybook/core-client';
6
6
  import { theme } from './components/Shared/theme';
7
- interface AsyncStorage {
8
- getItem: (key: string) => Promise<string | null>;
9
- setItem: (key: string, value: string) => Promise<void>;
7
+ interface InitialSelection {
8
+ /**
9
+ * Kind is the default export name or the storiesOf("name") name
10
+ */
11
+ kind: string;
12
+ /**
13
+ * Name is the named export or the .add("name") name
14
+ */
15
+ name: string;
10
16
  }
11
17
  export declare type Params = {
12
- onDeviceUI: boolean;
13
- asyncStorage: AsyncStorage | null;
14
- resetStorybook: boolean;
15
- disableWebsockets: boolean;
16
- query: string;
17
- host: string;
18
- port: number;
19
- secured: boolean;
20
- initialSelection: any;
21
- shouldPersistSelection: boolean;
22
- tabOpen: number;
23
- isUIHidden: boolean;
24
- shouldDisableKeyboardAvoidingView: boolean;
25
- keyboardAvoidingViewVerticalOffset: number;
18
+ onDeviceUI?: boolean;
19
+ resetStorybook?: boolean;
20
+ disableWebsockets?: boolean;
21
+ query?: string;
22
+ host?: string;
23
+ port?: number;
24
+ secured?: boolean;
25
+ initialSelection?: InitialSelection;
26
+ shouldPersistSelection?: boolean;
27
+ tabOpen?: number;
28
+ isUIHidden?: boolean;
29
+ shouldDisableKeyboardAvoidingView?: boolean;
30
+ keyboardAvoidingViewVerticalOffset?: number;
26
31
  } & {
27
- theme: typeof theme;
32
+ theme?: typeof theme;
28
33
  };
29
34
  export default class Preview {
30
35
  _clientApi: ClientApi;
@@ -33,18 +38,17 @@ export default class Preview {
33
38
  _channel: Channel;
34
39
  _decorators: any[];
35
40
  _asyncStorageStoryId: string;
36
- _asyncStorage: AsyncStorage | null;
37
41
  _configApi: ConfigApi;
38
42
  configure: (loadable: Loadable, m: NodeModule, showDeprecationWarning: boolean) => void;
39
43
  constructor();
40
44
  api: () => ClientApi;
41
45
  getStorybookUI: (params?: Partial<Params>) => () => JSX.Element;
42
- _setInitialStory: (initialSelection: any, shouldPersistSelection?: boolean) => Promise<void>;
43
- _getInitialStory: (initialSelection: any, shouldPersistSelection?: boolean) => Promise<import("@storybook/client-api").PublishedStoreItem>;
46
+ _setInitialStory: (initialSelection?: InitialSelection, shouldPersistSelection?: boolean) => Promise<void>;
47
+ _getInitialStory: (initialSelection?: InitialSelection, shouldPersistSelection?: boolean) => Promise<import("@storybook/client-api").PublishedStoreItem>;
44
48
  _getStory(storyId: string): import("@storybook/client-api").PublishedStoreItem;
45
49
  _selectStoryEvent({ storyId }: {
46
50
  storyId: string;
47
- }): void;
51
+ }, shouldPersistSelection: any): void;
48
52
  _selectStory(story: any): void;
49
53
  _checkStory(storyId: string): import("@storybook/client-api").PublishedStoreItem;
50
54
  }
@@ -7,15 +7,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
+ import AsyncStorage from '@react-native-async-storage/async-storage';
10
11
  import { addons } from '@storybook/addons';
11
12
  import Channel from '@storybook/channels';
12
13
  import { ClientApi, ConfigApi, StoryStore } from '@storybook/client-api';
13
14
  import Events from '@storybook/core-events';
15
+ import { toId } from '@storybook/csf';
14
16
  import { ThemeProvider } from 'emotion-theming';
15
17
  import React from 'react';
16
- import { loadCsf } from './loadCsf';
17
18
  import OnDeviceUI from './components/OnDeviceUI';
18
19
  import { theme } from './components/Shared/theme';
20
+ import { loadCsf } from './loadCsf';
19
21
  const STORAGE_KEY = 'lastOpenedStory';
20
22
  export default class Preview {
21
23
  constructor() {
@@ -23,20 +25,10 @@ export default class Preview {
23
25
  return this._clientApi;
24
26
  };
25
27
  this.getStorybookUI = (params = {}) => {
26
- if (params.asyncStorage === undefined) {
27
- console.warn(`
28
- Starting Storybook v5.3.0, we require you to manually pass an asyncStorage prop. Pass null to disable or use https://github.com/react-native-async-storage/async-storage.
29
-
30
- More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#react-native-async-storage
31
- `.trim());
32
- }
33
- if (params.asyncStorage) {
34
- this._asyncStorage = params.asyncStorage;
35
- }
36
- const { initialSelection, shouldPersistSelection } = params;
28
+ const { initialSelection, shouldPersistSelection = true } = params;
37
29
  this._setInitialStory(initialSelection, shouldPersistSelection);
38
30
  this._channel.on(Events.SET_CURRENT_STORY, (d) => {
39
- this._selectStoryEvent(d);
31
+ this._selectStoryEvent(d, shouldPersistSelection);
40
32
  });
41
33
  const { _storyStore } = this;
42
34
  addons.loadAddons(this._clientApi);
@@ -52,14 +44,17 @@ export default class Preview {
52
44
  });
53
45
  this._getInitialStory = (initialSelection, shouldPersistSelection = true) => __awaiter(this, void 0, void 0, function* () {
54
46
  let story = null;
55
- if (initialSelection && this._checkStory(initialSelection)) {
56
- story = initialSelection;
47
+ const initialSelectionId = initialSelection
48
+ ? toId(initialSelection.kind, initialSelection.name)
49
+ : undefined;
50
+ if (initialSelection && initialSelectionId && this._checkStory(initialSelectionId)) {
51
+ story = initialSelectionId;
57
52
  }
58
53
  else if (shouldPersistSelection) {
59
54
  try {
60
55
  let value = this._asyncStorageStoryId;
61
- if (!value && this._asyncStorage) {
62
- value = JSON.parse(yield this._asyncStorage.getItem(STORAGE_KEY));
56
+ if (!value) {
57
+ value = JSON.parse(yield AsyncStorage.getItem(STORAGE_KEY));
63
58
  this._asyncStorageStoryId = value;
64
59
  }
65
60
  if (this._checkStory(value)) {
@@ -96,10 +91,10 @@ export default class Preview {
96
91
  _getStory(storyId) {
97
92
  return this._storyStore.fromId(storyId);
98
93
  }
99
- _selectStoryEvent({ storyId }) {
94
+ _selectStoryEvent({ storyId }, shouldPersistSelection) {
100
95
  if (storyId) {
101
- if (this._asyncStorage) {
102
- this._asyncStorage.setItem(STORAGE_KEY, JSON.stringify(storyId)).catch(() => { });
96
+ if (shouldPersistSelection) {
97
+ AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(storyId)).catch(() => { });
103
98
  }
104
99
  const story = this._getStory(storyId);
105
100
  this._selectStory(story);
@@ -114,7 +109,7 @@ export default class Preview {
114
109
  return null;
115
110
  }
116
111
  const story = this._getStory(storyId);
117
- if (story.storyFn === null) {
112
+ if (story === null || story.storyFn === null) {
118
113
  return null;
119
114
  }
120
115
  return story;
@@ -11,12 +11,14 @@ import { getAddonPanelPosition, getNavigatorPanelPosition, getPreviewPosition, g
11
11
  import Navigation from './navigation';
12
12
  import { PREVIEW, ADDONS } from './navigation/constants';
13
13
  import Panel from './Panel';
14
+ import { useWindowDimensions } from 'react-native';
14
15
  const ANIMATION_DURATION = 300;
15
16
  const IS_IOS = Platform.OS === 'ios';
16
17
  // @ts-ignore: Property 'Expo' does not exist on type 'Global'
17
18
  const getExpoRoot = () => global.Expo || global.__expo || global.__exponent;
18
19
  export const IS_EXPO = getExpoRoot() !== undefined;
19
20
  const IS_ANDROID = Platform.OS === 'android';
21
+ const BREAKPOINT = 1024;
20
22
  const flex = { flex: 1 };
21
23
  const Preview = styled.View(flex, ({ disabled, theme }) => ({
22
24
  borderLeftWidth: disabled ? 0 : 1,
@@ -62,6 +64,7 @@ const OnDeviceUI = ({ storyStore, isUIHidden, shouldDisableKeyboardAvoidingView,
62
64
  });
63
65
  const story = useSelectedStory(storyStore);
64
66
  const animatedValue = useRef(new Animated.Value(tabOpen));
67
+ const wide = useWindowDimensions().width >= BREAKPOINT;
65
68
  const handleToggleTab = (newTabOpen) => {
66
69
  if (newTabOpen === tabOpen) {
67
70
  return;
@@ -81,9 +84,9 @@ const OnDeviceUI = ({ storyStore, isUIHidden, shouldDisableKeyboardAvoidingView,
81
84
  };
82
85
  const previewWrapperStyles = [
83
86
  flex,
84
- getPreviewPosition(animatedValue.current, previewDimensions, slideBetweenAnimation),
87
+ getPreviewPosition(animatedValue.current, previewDimensions, slideBetweenAnimation, wide),
85
88
  ];
86
- const previewStyles = [flex, getPreviewScale(animatedValue.current, slideBetweenAnimation)];
89
+ const previewStyles = [flex, getPreviewScale(animatedValue.current, slideBetweenAnimation, wide)];
87
90
  return (React.createElement(SafeAreaView, { style: [flex, IS_ANDROID && IS_EXPO && styles.expoAndroidContainer] },
88
91
  React.createElement(KeyboardAvoidingView, { enabled: !shouldDisableKeyboardAvoidingView || tabOpen !== PREVIEW, behavior: IS_IOS ? 'padding' : null, keyboardVerticalOffset: keyboardAvoidingViewVerticalOffset, style: flex },
89
92
  React.createElement(AbsolutePositionedKeyboardAwareView, { onLayout: setPreviewDimensions, previewDimensions: previewDimensions },
@@ -92,9 +95,9 @@ const OnDeviceUI = ({ storyStore, isUIHidden, shouldDisableKeyboardAvoidingView,
92
95
  React.createElement(Preview, { disabled: tabOpen === PREVIEW },
93
96
  React.createElement(StoryView, { story: story })),
94
97
  tabOpen !== PREVIEW ? (React.createElement(TouchableOpacity, { style: absolutePosition, onPress: () => handleToggleTab(PREVIEW) })) : null)),
95
- React.createElement(Panel, { style: getNavigatorPanelPosition(animatedValue.current, previewDimensions.width) },
98
+ React.createElement(Panel, { style: getNavigatorPanelPosition(animatedValue.current, previewDimensions.width, wide) },
96
99
  React.createElement(StoryListView, { storyStore: storyStore, selectedStory: story })),
97
- React.createElement(Panel, { style: getAddonPanelPosition(animatedValue.current, previewDimensions.width) },
100
+ React.createElement(Panel, { style: getAddonPanelPosition(animatedValue.current, previewDimensions.width, wide) },
98
101
  React.createElement(Addons, { active: tabOpen === ADDONS }))),
99
102
  React.createElement(Navigation, { tabOpen: tabOpen, onChangeTab: handleToggleTab, initialUiVisible: !isUIHidden }))));
100
103
  };
@@ -1,18 +1,18 @@
1
1
  import { Animated } from 'react-native';
2
2
  import { PreviewDimens } from './absolute-positioned-keyboard-aware-view';
3
- export declare const getNavigatorPanelPosition: (animatedValue: Animated.Value, previewWidth: number) => {
3
+ export declare const getNavigatorPanelPosition: (animatedValue: Animated.Value, previewWidth: number, wide: boolean) => {
4
4
  transform: {
5
5
  translateX: Animated.AnimatedInterpolation;
6
6
  }[];
7
7
  width: number;
8
8
  }[];
9
- export declare const getAddonPanelPosition: (animatedValue: Animated.Value, previewWidth: number) => {
9
+ export declare const getAddonPanelPosition: (animatedValue: Animated.Value, previewWidth: number, wide: boolean) => {
10
10
  transform: {
11
11
  translateX: Animated.AnimatedInterpolation;
12
12
  }[];
13
13
  width: number;
14
14
  }[];
15
- export declare const getPreviewPosition: (animatedValue: Animated.Value, { width: previewWidth, height: previewHeight }: PreviewDimens, slideBetweenAnimation: boolean) => {
15
+ export declare const getPreviewPosition: (animatedValue: Animated.Value, { width: previewWidth, height: previewHeight }: PreviewDimens, slideBetweenAnimation: boolean, wide: boolean) => {
16
16
  transform: ({
17
17
  translateX: Animated.AnimatedInterpolation;
18
18
  translateY?: undefined;
@@ -21,7 +21,7 @@ export declare const getPreviewPosition: (animatedValue: Animated.Value, { width
21
21
  translateX?: undefined;
22
22
  })[];
23
23
  };
24
- export declare const getPreviewScale: (animatedValue: Animated.Value, slideBetweenAnimation: boolean) => {
24
+ export declare const getPreviewScale: (animatedValue: Animated.Value, slideBetweenAnimation: boolean, wide: boolean) => {
25
25
  transform: {
26
26
  scale: Animated.AnimatedInterpolation;
27
27
  }[];
@@ -1,39 +1,47 @@
1
1
  import { NAVIGATOR, PREVIEW, ADDONS } from './navigation/constants';
2
2
  const PREVIEW_SCALE = 0.3;
3
- const panelWidth = (width) => width * (1 - PREVIEW_SCALE - 0.05);
4
- export const getNavigatorPanelPosition = (animatedValue, previewWidth) => {
3
+ const PREVIEW_WIDE_SCREEN = 0.7;
4
+ const SCALE_OFFSET = 0.025;
5
+ const TRANSLATE_X_OFFSET = 6;
6
+ const TRANSLATE_Y_OFFSET = 12;
7
+ const panelWidth = (width, wide) => {
8
+ const scale = wide ? PREVIEW_WIDE_SCREEN : PREVIEW_SCALE;
9
+ return width * (1 - scale - SCALE_OFFSET);
10
+ };
11
+ export const getNavigatorPanelPosition = (animatedValue, previewWidth, wide) => {
5
12
  return [
6
13
  {
7
14
  transform: [
8
15
  {
9
16
  translateX: animatedValue.interpolate({
10
17
  inputRange: [NAVIGATOR, PREVIEW],
11
- outputRange: [0, -panelWidth(previewWidth) - 1],
18
+ outputRange: [0, -panelWidth(previewWidth, wide) - 1],
12
19
  }),
13
20
  },
14
21
  ],
15
- width: panelWidth(previewWidth),
22
+ width: panelWidth(previewWidth, wide),
16
23
  },
17
24
  ];
18
25
  };
19
- export const getAddonPanelPosition = (animatedValue, previewWidth) => {
26
+ export const getAddonPanelPosition = (animatedValue, previewWidth, wide) => {
20
27
  return [
21
28
  {
22
29
  transform: [
23
30
  {
24
31
  translateX: animatedValue.interpolate({
25
32
  inputRange: [PREVIEW, ADDONS],
26
- outputRange: [previewWidth, previewWidth - panelWidth(previewWidth)],
33
+ outputRange: [previewWidth, previewWidth - panelWidth(previewWidth, wide)],
27
34
  }),
28
35
  },
29
36
  ],
30
- width: panelWidth(previewWidth),
37
+ width: panelWidth(previewWidth, wide),
31
38
  },
32
39
  ];
33
40
  };
34
- export const getPreviewPosition = (animatedValue, { width: previewWidth, height: previewHeight }, slideBetweenAnimation) => {
35
- const translateX = previewWidth / 2 - (previewWidth * PREVIEW_SCALE) / 2 - 6;
36
- const translateY = -(previewHeight / 2 - (previewHeight * PREVIEW_SCALE) / 2 - 12);
41
+ export const getPreviewPosition = (animatedValue, { width: previewWidth, height: previewHeight }, slideBetweenAnimation, wide) => {
42
+ const scale = wide ? PREVIEW_WIDE_SCREEN : PREVIEW_SCALE;
43
+ const translateX = previewWidth / 2 - (previewWidth * scale) / 2 - TRANSLATE_X_OFFSET;
44
+ const translateY = -(previewHeight / 2 - (previewHeight * scale) / 2 - TRANSLATE_Y_OFFSET);
37
45
  return {
38
46
  transform: [
39
47
  {
@@ -51,13 +59,14 @@ export const getPreviewPosition = (animatedValue, { width: previewWidth, height:
51
59
  ],
52
60
  };
53
61
  };
54
- export const getPreviewScale = (animatedValue, slideBetweenAnimation) => {
62
+ export const getPreviewScale = (animatedValue, slideBetweenAnimation, wide) => {
63
+ const scale = wide ? PREVIEW_WIDE_SCREEN : PREVIEW_SCALE;
55
64
  return {
56
65
  transform: [
57
66
  {
58
67
  scale: animatedValue.interpolate({
59
68
  inputRange: [NAVIGATOR, PREVIEW, ADDONS],
60
- outputRange: [PREVIEW_SCALE, slideBetweenAnimation ? PREVIEW_SCALE : 1, PREVIEW_SCALE],
69
+ outputRange: [scale, slideBetweenAnimation ? scale : 1, scale],
61
70
  }),
62
71
  },
63
72
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storybook/react-native",
3
- "version": "6.0.1-alpha.3",
3
+ "version": "6.0.1-beta.0",
4
4
  "description": "A better way to develop React Native Components for your app",
5
5
  "keywords": [
6
6
  "react",
@@ -19,8 +19,8 @@
19
19
  "license": "MIT",
20
20
  "main": "dist/index.js",
21
21
  "bin": {
22
- "sbn-get-stories": "./bin/get-stories.js",
23
- "sbn-watcher": "./bin/watcher.js"
22
+ "sb-rn-get-stories": "./bin/get-stories.js",
23
+ "sb-rn-watcher": "./bin/watcher.js"
24
24
  },
25
25
  "files": [
26
26
  "bin/**/*",
@@ -32,29 +32,47 @@
32
32
  ],
33
33
  "scripts": {
34
34
  "preprepare": "rm -rf dist/",
35
- "prepare": "tsc"
35
+ "prepare": "tsc",
36
+ "test": "jest"
37
+ },
38
+ "jest": {
39
+ "moduleFileExtensions": [
40
+ "ts",
41
+ "tsx",
42
+ "js",
43
+ "jsx",
44
+ "json",
45
+ "node"
46
+ ],
47
+ "preset": "react-native"
36
48
  },
37
49
  "dependencies": {
38
50
  "@emotion/core": "^10.0.20",
39
51
  "@emotion/native": "^10.0.14",
40
- "@storybook/addons": "^6",
41
- "@storybook/channel-websocket": "^6",
42
- "@storybook/channels": "^6",
43
- "@storybook/client-api": "^6",
44
- "@storybook/client-logger": "^6",
45
- "@storybook/core-client": "^6",
46
- "@storybook/core-events": "^6",
52
+ "@storybook/addons": "~6.3",
53
+ "@storybook/channel-websocket": "~6.3",
54
+ "@storybook/channels": "~6.3",
55
+ "@storybook/client-api": "~6.3",
56
+ "@storybook/client-logger": "~6.3",
57
+ "@storybook/core-client": "~6.3",
58
+ "@storybook/core-events": "~6.3",
47
59
  "@storybook/csf": "0.0.1",
48
60
  "chokidar": "^3.5.1",
61
+ "commander": "^8.2.0",
49
62
  "emotion-theming": "^10.0.19",
50
63
  "glob": "^7.1.7",
64
+ "prettier": "^2.4.1",
51
65
  "react-native-swipe-gestures": "^1.0.5",
52
66
  "util": "^0.12.4"
53
67
  },
54
68
  "devDependencies": {
55
- "@types/react-native": "^0.64.6"
69
+ "@types/react-native": "^0.66.15",
70
+ "babel-jest": "^26.6.3",
71
+ "jest": "^26.6.3",
72
+ "react-test-renderer": "17.0.2"
56
73
  },
57
74
  "peerDependencies": {
75
+ "@react-native-async-storage/async-storage": ">=1",
58
76
  "react": "*",
59
77
  "react-native": ">=0.57.0"
60
78
  },
@@ -64,5 +82,5 @@
64
82
  "publishConfig": {
65
83
  "access": "public"
66
84
  },
67
- "gitHead": "11deb93dc1c4aa183624c3b1df98b5882ccd53a3"
85
+ "gitHead": "3490aec85c84210997f38b92afa515b6dc41b8b5"
68
86
  }
@@ -0,0 +1,118 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`loader writeRequires when there is a story glob and exclude paths globs writes the story imports 1`] = `
4
+ "
5
+ /* do not change this file, it is auto generated by storybook. */
6
+
7
+ import { configure, addDecorator, addParameters, addArgsEnhancer } from '@storybook/react-native';
8
+
9
+ import \\"@storybook/addon-ondevice-notes/register\\";
10
+ import \\"@storybook/addon-ondevice-controls/register\\";
11
+ import \\"@storybook/addon-ondevice-backgrounds/register\\";
12
+ import \\"@storybook/addon-ondevice-actions/register\\";
13
+
14
+ import { argsEnhancers } from \\"@storybook/addon-actions/dist/modern/preset/addArgs\\"
15
+
16
+
17
+ import { decorators, parameters } from './preview';
18
+
19
+ if (decorators) {
20
+ decorators.forEach((decorator) => addDecorator(decorator));
21
+ }
22
+
23
+ if (parameters) {
24
+ addParameters(parameters);
25
+ }
26
+
27
+
28
+ argsEnhancers.forEach(enhancer => addArgsEnhancer(enhancer))
29
+
30
+ const getStories=() => {
31
+ return [require(\\"include-components/FakeStory.stories.tsx\\")];
32
+ }
33
+
34
+ configure(getStories, module, false)
35
+ "
36
+ `;
37
+
38
+ exports[`loader writeRequires when there is a story glob writes the story imports 1`] = `
39
+ "
40
+ /* do not change this file, it is auto generated by storybook. */
41
+
42
+ import { configure, addDecorator, addParameters, addArgsEnhancer } from '@storybook/react-native';
43
+
44
+ import \\"@storybook/addon-ondevice-notes/register\\";
45
+ import \\"@storybook/addon-ondevice-controls/register\\";
46
+ import \\"@storybook/addon-ondevice-backgrounds/register\\";
47
+ import \\"@storybook/addon-ondevice-actions/register\\";
48
+
49
+ import { argsEnhancers } from \\"@storybook/addon-actions/dist/modern/preset/addArgs\\"
50
+
51
+
52
+ import { decorators, parameters } from './preview';
53
+
54
+ if (decorators) {
55
+ decorators.forEach((decorator) => addDecorator(decorator));
56
+ }
57
+
58
+ if (parameters) {
59
+ addParameters(parameters);
60
+ }
61
+
62
+
63
+ argsEnhancers.forEach(enhancer => addArgsEnhancer(enhancer))
64
+
65
+ const getStories=() => {
66
+ return [require(\\"./FakeStory.stories.tsx\\")];
67
+ }
68
+
69
+ configure(getStories, module, false)
70
+ "
71
+ `;
72
+
73
+ exports[`loader writeRequires when there is no preview does not add preview related stuff 1`] = `
74
+ "
75
+ /* do not change this file, it is auto generated by storybook. */
76
+
77
+ import { configure, addDecorator, addParameters, addArgsEnhancer } from '@storybook/react-native';
78
+
79
+ import \\"@storybook/addon-ondevice-notes/register\\";
80
+ import \\"@storybook/addon-ondevice-controls/register\\";
81
+ import \\"@storybook/addon-ondevice-backgrounds/register\\";
82
+ import \\"@storybook/addon-ondevice-actions/register\\";
83
+
84
+ import { argsEnhancers } from \\"@storybook/addon-actions/dist/modern/preset/addArgs\\"
85
+
86
+
87
+
88
+ argsEnhancers.forEach(enhancer => addArgsEnhancer(enhancer))
89
+
90
+ const getStories=() => {
91
+ return [require(\\"./FakeStory.stories.tsx\\")];
92
+ }
93
+
94
+ configure(getStories, module, false)
95
+ "
96
+ `;
97
+
98
+ exports[`loader writeRequires when there is no story glob or addons writes no story imports or addons 1`] = `
99
+ "
100
+ /* do not change this file, it is auto generated by storybook. */
101
+
102
+ import { configure, addDecorator, addParameters, addArgsEnhancer } from '@storybook/react-native';
103
+
104
+
105
+
106
+
107
+
108
+
109
+
110
+
111
+
112
+ const getStories=() => {
113
+ return [];
114
+ }
115
+
116
+ configure(getStories, module, false)
117
+ "
118
+ `;
@@ -1,3 +1,5 @@
1
1
  const { writeRequires } = require('./loader');
2
+ const { getArguments } = require('./handle-args');
3
+ const args = getArguments();
2
4
 
3
- writeRequires();
5
+ writeRequires(args);
@@ -0,0 +1,18 @@
1
+ function getArguments() {
2
+ const { program } = require('commander');
3
+
4
+ program
5
+ .description('Getter and watcher for react native storybook')
6
+ .option(
7
+ '-c, --config-path <path>',
8
+ 'The path to your config folder relative to your project-dir',
9
+ './.storybook'
10
+ )
11
+ .option('-a, --absolute', 'Use absolute paths for story imports');
12
+
13
+ program.parse();
14
+
15
+ return program.opts();
16
+ }
17
+
18
+ module.exports = { getArguments };
package/scripts/loader.js CHANGED
@@ -1,98 +1,115 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
3
  const glob = require('glob');
4
- const storybookRequiresLocation = '/.storybook/storybook.requires.js';
4
+ const prettier = require('prettier');
5
5
 
6
- function requireUncached(module) {
7
- delete require.cache[require.resolve(module)];
8
- return require(module);
9
- }
6
+ const cwd = process.cwd();
10
7
 
11
- function getMainPath() {
12
- const cwd = process.cwd();
13
- return path.join(cwd, '.storybook/main.js');
14
- }
8
+ const previewImports = `
9
+ import { decorators, parameters } from './preview';
15
10
 
16
- function getPreviewPath() {
17
- const cwd = process.cwd();
18
- return path.join(cwd, '.storybook/preview.js');
19
- }
11
+ if (decorators) {
12
+ decorators.forEach((decorator) => addDecorator(decorator));
13
+ }
20
14
 
21
- function getPreviewExists() {
22
- return fs.existsSync(getPreviewPath());
23
- }
15
+ if (parameters) {
16
+ addParameters(parameters);
17
+ }
18
+ `;
24
19
 
25
- function getGlobs() {
26
- // we need to invalidate the cache because otherwise the latest changes don't get loaded
27
- const { stories: storyGlobs } = requireUncached(getMainPath());
20
+ function normalizeExcludePaths(paths) {
21
+ // automatically convert a string to an array of a single string
22
+ if (typeof paths === 'string') {
23
+ return [paths];
24
+ }
25
+
26
+ // ensure the paths is an array and if any items exists, they are strings
27
+ if (Array.isArray(paths) && paths.every((p) => typeof p === 'string')) {
28
+ return paths;
29
+ }
28
30
 
29
- return storyGlobs;
31
+ // when the paths aren't a string or an (empty) array of strings, return
32
+ return undefined;
30
33
  }
31
34
 
32
- function getAddons() {
33
- const { addons } = requireUncached(getMainPath());
35
+ function requireUncached(module) {
36
+ delete require.cache[require.resolve(module)];
37
+ return require(module);
38
+ }
34
39
 
35
- return addons;
40
+ function getMain({ configPath }) {
41
+ const mainPath = path.resolve(cwd, configPath, 'main.js');
42
+ return requireUncached(mainPath);
36
43
  }
37
44
 
38
- function getPaths() {
39
- return getGlobs().reduce((acc, storyGlob) => {
40
- const paths = glob.sync(storyGlob);
41
- return [...acc, ...paths.map((storyPath) => `../${storyPath}`)];
42
- }, []);
45
+ function getPreviewExists({ configPath }) {
46
+ const previewPath = path.resolve(cwd, configPath, 'preview.js');
47
+ return fs.existsSync(previewPath);
43
48
  }
44
49
 
45
- function writeRequires() {
46
- const cwd = process.cwd();
50
+ function writeRequires({ configPath, absolute = false }) {
51
+ const storybookRequiresLocation = path.resolve(cwd, configPath, 'storybook.requires.js');
52
+
53
+ const main = getMain({ configPath });
54
+ const reactNativeOptions = main.reactNativeOptions;
55
+ const excludePaths = reactNativeOptions && reactNativeOptions.excludePaths;
56
+ const normalizedExcludePaths = normalizeExcludePaths(excludePaths);
57
+
58
+ const storyPaths = main.stories.reduce((acc, storyGlob) => {
59
+ const paths = glob.sync(storyGlob, {
60
+ cwd: path.resolve(cwd, configPath),
61
+ absolute,
62
+ // default to always ignore (exclude) anything in node_modules
63
+ ignore: normalizedExcludePaths !== undefined
64
+ ? normalizedExcludePaths
65
+ : ['**/node_modules'],
66
+ });
67
+ return [...acc, ...paths];
68
+ }, []);
47
69
 
48
- const storyPaths = getPaths();
49
- const addons = getAddons();
70
+ fs.writeFileSync(storybookRequiresLocation, '');
50
71
 
51
- fs.writeFileSync(path.join(cwd, storybookRequiresLocation), '');
72
+ const previewExists = getPreviewExists({ configPath });
52
73
 
53
- const previewExists = getPreviewExists();
54
- let previewJs = previewExists
55
- ? `
56
- import { decorators, parameters } from './preview';
57
- if (decorators) {
58
- decorators.forEach((decorator) => addDecorator(decorator));
59
- }
60
- if (parameters) {
61
- addParameters(parameters);
62
- }`
63
- : '';
74
+ let previewJs = previewExists ? previewImports : '';
64
75
 
65
- const storyRequires = storyPaths.map((storyPath) => `\t\trequire("${storyPath}")`).join(', \n');
66
- const path_array_str = `[\n${storyRequires}\n\t]`;
76
+ const storyRequires = storyPaths.map((storyPath) => `require("${storyPath}")`).join(',');
77
+ const path_array_str = `[${storyRequires}]`;
67
78
 
68
- const registerAddons = addons.map((addon) => `import "${addon}/register";`).join('\n');
79
+ const registerAddons = main.addons.map((addon) => `import "${addon}/register";`).join('\n');
80
+ let enhancersImport = '';
69
81
  let enhancers = '';
70
82
 
71
83
  // TODO: implement presets or something similar
72
- if (addons.includes('@storybook/addon-ondevice-actions')) {
73
- enhancers = `import { argsEnhancers } from '@storybook/addon-actions/dist/modern/preset/addArgs';
74
- argsEnhancers.forEach(enhancer => addArgsEnhancer(enhancer))`;
84
+ if (main.addons.includes('@storybook/addon-ondevice-actions')) {
85
+ enhancersImport =
86
+ 'import { argsEnhancers } from "@storybook/addon-actions/dist/modern/preset/addArgs"';
87
+ enhancers = 'argsEnhancers.forEach(enhancer => addArgsEnhancer(enhancer))';
75
88
  }
76
89
 
77
90
  const fileContent = `
78
- /*
79
- do not change this file, it is auto generated by storybook.
80
- */
81
- import { configure, addDecorator, addParameters, addArgsEnhancer } from '@storybook/react-native';
82
- ${registerAddons}
91
+ /* do not change this file, it is auto generated by storybook. */
83
92
 
84
- ${enhancers}
93
+ import { configure, addDecorator, addParameters, addArgsEnhancer } from '@storybook/react-native';
85
94
 
86
- ${previewJs}
95
+ ${registerAddons}
87
96
 
88
- const getStories=() => {
89
- return ${path_array_str};
90
- }
91
- configure(getStories, module, false)
97
+ ${enhancersImport}
98
+
99
+ ${previewJs}
92
100
 
101
+ ${enhancers}
102
+
103
+ const getStories=() => {
104
+ return ${path_array_str};
105
+ }
106
+
107
+ configure(getStories, module, false)
93
108
  `;
94
109
 
95
- fs.writeFileSync(path.join(cwd, storybookRequiresLocation), fileContent, {
110
+ const formattedFileContent = prettier.format(fileContent, { parser: 'babel' });
111
+
112
+ fs.writeFileSync(storybookRequiresLocation, formattedFileContent, {
96
113
  encoding: 'utf8',
97
114
  flag: 'w',
98
115
  });
@@ -100,8 +117,6 @@ configure(getStories, module, false)
100
117
 
101
118
  module.exports = {
102
119
  writeRequires,
103
- getGlobs,
104
- getMainPath,
120
+ getMain,
105
121
  getPreviewExists,
106
- getPreviewPath,
107
122
  };
@@ -0,0 +1,133 @@
1
+ const path = require('path');
2
+ const { writeRequires, getMain, getPreviewExists } = require('./loader');
3
+ const glob = require('glob');
4
+
5
+ let pathMock;
6
+ let fileContentMock;
7
+
8
+ jest.mock('fs', () => ({
9
+ ...jest.requireActual('fs'),
10
+ writeFileSync: (filePath, fileContent, opts) => {
11
+ pathMock = filePath;
12
+ fileContentMock = fileContent;
13
+ },
14
+ }));
15
+
16
+ jest.mock('prettier', () => ({
17
+ format(s, opts) {
18
+ return s;
19
+ },
20
+ }));
21
+
22
+ describe('loader', () => {
23
+ describe('getMain', () => {
24
+ it('should return the main js default export as an object', () => {
25
+ const main = getMain({ configPath: path.resolve(__dirname, 'mocks/all-config-files') });
26
+ expect(main).toEqual({
27
+ stories: ['./FakeStory.stories.tsx'],
28
+ addons: [
29
+ '@storybook/addon-ondevice-notes',
30
+ '@storybook/addon-ondevice-controls',
31
+ '@storybook/addon-ondevice-backgrounds',
32
+ '@storybook/addon-ondevice-actions',
33
+ ],
34
+ });
35
+ });
36
+
37
+ it('should also work with relative paths', () => {
38
+ // relative from where the command is run
39
+ const main = getMain({ configPath: './scripts/mocks/all-config-files' });
40
+ expect(main).toEqual({
41
+ stories: ['./FakeStory.stories.tsx'],
42
+ addons: [
43
+ '@storybook/addon-ondevice-notes',
44
+ '@storybook/addon-ondevice-controls',
45
+ '@storybook/addon-ondevice-backgrounds',
46
+ '@storybook/addon-ondevice-actions',
47
+ ],
48
+ });
49
+ });
50
+ });
51
+
52
+ describe('getPreviewExists', () => {
53
+ describe('when using a relative path', () => {
54
+ it('should return true if the preview exists', () => {
55
+ expect(getPreviewExists({ configPath: 'scripts/mocks/all-config-files' })).toBe(true);
56
+ });
57
+
58
+ it('should return false if the preview does not exist', () => {
59
+ expect(getPreviewExists({ configPath: './scripts/mocks/no-preview' })).toBe(false);
60
+ });
61
+ });
62
+
63
+ describe('when using an absolute path', () => {
64
+ it('should return true if the preview exists', () => {
65
+ expect(
66
+ getPreviewExists({ configPath: path.resolve(__dirname, 'mocks/all-config-files') })
67
+ ).toBe(true);
68
+ });
69
+
70
+ it('should return false if the preview does not exist', () => {
71
+ expect(getPreviewExists({ configPath: path.resolve(__dirname, 'mocks/no-preview') })).toBe(
72
+ false
73
+ );
74
+ });
75
+ });
76
+ });
77
+
78
+ describe('writeRequires', () => {
79
+ describe('when there is a story glob', () => {
80
+ it('writes the story imports', () => {
81
+ writeRequires({ configPath: 'scripts/mocks/all-config-files' });
82
+ expect(pathMock).toEqual(
83
+ path.resolve(__dirname, 'mocks/all-config-files/storybook.requires.js')
84
+ );
85
+ expect(fileContentMock).toMatchSnapshot();
86
+ });
87
+ });
88
+
89
+ describe('when there is a story glob and exclude paths globs', () => {
90
+ it('writes the story imports', () => {
91
+ writeRequires({ configPath: 'scripts/mocks/exclude-config-files' });
92
+ expect(pathMock).toEqual(
93
+ path.resolve(__dirname, 'mocks/exclude-config-files/storybook.requires.js')
94
+ );
95
+
96
+ expect(fileContentMock).toContain('include-components/FakeStory.stories.tsx');
97
+ expect(fileContentMock).not.toContain('exclude-components/FakeStory.stories.tsx');
98
+
99
+ expect(fileContentMock).toMatchSnapshot();
100
+ });
101
+ });
102
+
103
+ describe('when there is no story glob or addons', () => {
104
+ it('writes no story imports or addons', () => {
105
+ writeRequires({ configPath: 'scripts/mocks/blank-config' });
106
+ expect(pathMock).toEqual(
107
+ path.resolve(__dirname, 'mocks/blank-config/storybook.requires.js')
108
+ );
109
+ expect(fileContentMock).toMatchSnapshot();
110
+ });
111
+ });
112
+
113
+ describe('when there is no preview', () => {
114
+ it('does not add preview related stuff', () => {
115
+ writeRequires({ configPath: 'scripts/mocks/no-preview' });
116
+ expect(pathMock).toEqual(path.resolve(__dirname, 'mocks/no-preview/storybook.requires.js'));
117
+ expect(fileContentMock).toMatchSnapshot();
118
+ });
119
+ });
120
+
121
+ describe('when the absolute option is true', () => {
122
+ it('should write absolute paths to the requires file', () => {
123
+ writeRequires({ configPath: 'scripts/mocks/all-config-files', absolute: true });
124
+ expect(pathMock).toEqual(
125
+ path.resolve(__dirname, 'mocks/all-config-files/storybook.requires.js')
126
+ );
127
+ expect(fileContentMock).toContain(
128
+ path.resolve(__dirname, 'mocks/all-config-files/FakeStory.stories.tsx')
129
+ );
130
+ });
131
+ });
132
+ });
133
+ });
@@ -0,0 +1 @@
1
+ export const FakeComponent = () => null;
@@ -0,0 +1,10 @@
1
+ import { FakeComponent } from './FakeComponent';
2
+
3
+ export default {
4
+ title: 'components/FakeComponent',
5
+ component: FakeComponent,
6
+ };
7
+
8
+ export const Basic = {
9
+ args: {},
10
+ };
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ stories: ['./FakeStory.stories.tsx'],
3
+ addons: [
4
+ '@storybook/addon-ondevice-notes',
5
+ '@storybook/addon-ondevice-controls',
6
+ '@storybook/addon-ondevice-backgrounds',
7
+ '@storybook/addon-ondevice-actions',
8
+ ],
9
+ };
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds';
4
+
5
+ export const decorators = [
6
+ (StoryFn) => (
7
+ <View style={styles.container}>
8
+ <StoryFn />
9
+ </View>
10
+ ),
11
+ withBackgrounds,
12
+ ];
13
+ export const parameters = {
14
+ my_param: 'anything',
15
+ backgrounds: [
16
+ { name: 'plain', value: 'white', default: true },
17
+ { name: 'warm', value: 'hotpink' },
18
+ { name: 'cool', value: 'deepskyblue' },
19
+ ],
20
+ };
21
+
22
+ const styles = StyleSheet.create({
23
+ container: { padding: 8, flex: 1 },
24
+ });
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ stories: [],
3
+ addons: [],
4
+ };
@@ -0,0 +1 @@
1
+ export const FakeComponent = () => null;
@@ -0,0 +1,10 @@
1
+ import { FakeComponentExcluded } from './FakeComponent';
2
+
3
+ export default {
4
+ title: 'components/FakeComponentExcluded',
5
+ component: FakeComponentExcluded,
6
+ };
7
+
8
+ export const Basic = {
9
+ args: {},
10
+ };
@@ -0,0 +1 @@
1
+ export const FakeComponent = () => null;
@@ -0,0 +1,10 @@
1
+ import { FakeComponent } from './FakeComponent';
2
+
3
+ export default {
4
+ title: 'components/FakeComponent',
5
+ component: FakeComponent,
6
+ };
7
+
8
+ export const Basic = {
9
+ args: {},
10
+ };
@@ -0,0 +1,12 @@
1
+ module.exports = {
2
+ stories: ['**/*.stories.tsx'],
3
+ reactNativeOptions: {
4
+ excludePaths: '**/exclude-components/**',
5
+ },
6
+ addons: [
7
+ '@storybook/addon-ondevice-notes',
8
+ '@storybook/addon-ondevice-controls',
9
+ '@storybook/addon-ondevice-backgrounds',
10
+ '@storybook/addon-ondevice-actions',
11
+ ],
12
+ };
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds';
4
+
5
+ export const decorators = [
6
+ (StoryFn) => (
7
+ <View style={styles.container}>
8
+ <StoryFn />
9
+ </View>
10
+ ),
11
+ withBackgrounds,
12
+ ];
13
+ export const parameters = {
14
+ my_param: 'anything',
15
+ backgrounds: [
16
+ { name: 'plain', value: 'white', default: true },
17
+ { name: 'warm', value: 'hotpink' },
18
+ { name: 'cool', value: 'deepskyblue' },
19
+ ],
20
+ };
21
+
22
+ const styles = StyleSheet.create({
23
+ container: { padding: 8, flex: 1 },
24
+ });
@@ -0,0 +1 @@
1
+ export const FakeComponent = () => null;
@@ -0,0 +1,10 @@
1
+ import { FakeComponent } from './FakeComponent';
2
+
3
+ export default {
4
+ title: 'components/FakeComponent',
5
+ component: FakeComponent,
6
+ };
7
+
8
+ export const Basic = {
9
+ args: {},
10
+ };
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ stories: ['./FakeStory.stories.tsx'],
3
+ addons: [
4
+ '@storybook/addon-ondevice-notes',
5
+ '@storybook/addon-ondevice-controls',
6
+ '@storybook/addon-ondevice-backgrounds',
7
+ '@storybook/addon-ondevice-actions',
8
+ ],
9
+ };
@@ -1,29 +1,62 @@
1
1
  const chokidar = require('chokidar');
2
2
  const path = require('path');
3
- const {
4
- getGlobs,
5
- writeRequires,
6
- getMainPath,
7
- getPreviewPath,
8
- getPreviewExists,
9
- } = require('./loader');
10
3
 
4
+ const { writeRequires, getMain, getPreviewExists } = require('./loader');
5
+
6
+ const { getArguments } = require('./handle-args');
7
+
8
+ const args = getArguments();
11
9
  const log = console.log.bind(console);
12
10
 
13
- const watchPaths = [getMainPath()];
14
- if (getPreviewExists()) {
15
- watchPaths.push(getPreviewPath());
11
+ const watchPaths = ['./main.js'];
12
+
13
+ if (getPreviewExists(args)) {
14
+ watchPaths.push('./preview.js');
16
15
  }
17
16
 
18
17
  const updateRequires = (event, watchPath) => {
19
18
  if (typeof watchPath === 'string') {
20
19
  log(`event ${event} for file ${path.basename(watchPath)}`);
21
20
  }
22
- writeRequires();
21
+ writeRequires(args);
23
22
  };
24
23
 
25
- chokidar.watch(watchPaths).on('change', (watchPath) => updateRequires('change', watchPath));
24
+ const globs = getMain(args).stories;
25
+
26
+ chokidar
27
+ .watch(watchPaths, { cwd: args.configPath })
28
+ .on('change', (watchPath) => updateRequires('change', watchPath));
29
+
30
+ let isReady = false;
31
+
26
32
  chokidar
27
- .watch(getGlobs())
28
- .on('add', (watchPath) => updateRequires('add', watchPath))
29
- .on('unlink', (watchPath) => updateRequires('unlink', watchPath));
33
+ .watch(globs, { cwd: args.configPath })
34
+ .on('ready', () => {
35
+ log('Watcher is ready, performing initial write');
36
+ writeRequires(args);
37
+ log('Waiting for changes, press r to manually re-write');
38
+ isReady = true;
39
+ })
40
+ .on('add', (watchPath) => {
41
+ if (isReady) {
42
+ updateRequires('add', watchPath);
43
+ }
44
+ })
45
+ .on('unlink', (watchPath) => {
46
+ if (isReady) {
47
+ updateRequires('unlink', watchPath);
48
+ }
49
+ });
50
+
51
+ const readline = require('readline');
52
+ readline.emitKeypressEvents(process.stdin);
53
+ process.stdin.setRawMode(true);
54
+ process.stdin.on('keypress', (str, key) => {
55
+ if (key.ctrl && key.name === 'c') {
56
+ process.exit();
57
+ }
58
+ if (key.name === 'r') {
59
+ log('Detected "r" keypress, rewriting story imports...');
60
+ writeRequires(args);
61
+ }
62
+ });