File size: 2,583 Bytes
d35f64e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import path from 'path';
import { useStore } from '@nanostores/react';
import sdk from '@stackblitz/sdk';
import type { FileMap } from '~/lib/stores/files';
import { workbenchStore, type ArtifactState } from '~/lib/stores/workbench';
import { WORK_DIR } from '~/utils/constants';
import { memo, useCallback, useEffect, useState } from 'react';
import type { ActionState } from '~/lib/runtime/action-runner';

// return false if some file-writing actions haven't completed
const fileActionsComplete = (actions: Record<string, ActionState>) => {
  return !Object.values(actions).some((action) => action.type === 'file' && action.status !== 'complete');
};

// extract relative path and content from file, wrapped in array for flatMap use
const extractContent = ([file, value]: [string, FileMap[string]]) => {
  // ignore directory entries
  if (!value || value.type !== 'file') {
    return [];
  }

  const relative = path.relative(WORK_DIR, file);
  const parts = relative.split(path.sep);

  // ignore hidden files
  if (parts.some((part) => part.startsWith('.'))) {
    return [];
  }

  return [[relative, value.content]];
};

// subscribe to changes in first artifact's runner actions
const useFirstArtifact = (): [boolean, ArtifactState] => {
  const [hasLoaded, setHasLoaded] = useState(false);
  const artifacts = useStore(workbenchStore.artifacts);
  const firstArtifact = artifacts[workbenchStore.artifactList[0]];

  const handleActionChange = useCallback(
    (actions: Record<string, ActionState>) => setHasLoaded(fileActionsComplete(actions)),
    [firstArtifact],
  );

  useEffect(() => {
    if (firstArtifact) {
      return firstArtifact.runner.actions.subscribe(handleActionChange);
    }

    return undefined;
  }, [firstArtifact]);

  return [hasLoaded, firstArtifact];
};

export const OpenStackBlitz = memo(() => {
  const [artifactLoaded, artifact] = useFirstArtifact();

  const handleClick = useCallback(() => {
    // extract relative path and content from files map
    const workbenchFiles = workbenchStore.files.get();
    const files = Object.fromEntries(Object.entries(workbenchFiles).flatMap(extractContent));

    // we use the first artifact's title for the StackBlitz project
    const { title } = artifact;

    sdk.openProject({
      title,
      template: 'node',
      files,
    });
  }, [artifact]);

  if (!artifactLoaded) {
    return null;
  }

  return (
    <a onClick={handleClick} className="cursor-pointer">
      <img alt="Open in StackBlitz" src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" />
    </a>
  );
});