├── .eslintrc.js
├── .github
└── workflows
│ └── dispatch.yml
├── .gitignore
├── .prettierrc
├── .vscode
├── css_custom_data.json
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── .vscodeignore
├── LICENSE
├── README.md
├── demo.gif
├── docs
├── icon-transparent.png
└── icon.png
├── git.d.ts
├── package.json
├── postcss.config.js
├── screenshots
├── command-panel.png
├── cron-editor.png
├── gui-view.png
├── http-step.png
├── show-preview-button.png
└── sql-step.png
├── snowpack.config.js
├── src
├── extension.ts
├── flatConfigEditor.ts
├── git.ts
├── lib.ts
├── types.ts
└── webviews
│ ├── src
│ ├── App.tsx
│ ├── Header.tsx
│ ├── Jobs.tsx
│ ├── Setting.tsx
│ ├── Step.tsx
│ ├── StepConfig.tsx
│ ├── Triggers.tsx
│ ├── VSCodeAPI.tsx
│ ├── Workflow.tsx
│ ├── error-state.tsx
│ ├── index.tsx
│ ├── settings
│ │ ├── CronChooser.tsx
│ │ ├── FieldWithDescription.tsx
│ │ ├── FilePicker.tsx
│ │ ├── FilePreview.tsx
│ │ ├── HttpEndpointPreview.tsx
│ │ └── SecretInput.tsx
│ ├── store.ts
│ ├── validation.ts
│ └── vscode.css
│ └── types
│ └── static.d.ts
├── tailwind.config.js
├── tsconfig-webview.json
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /**@type {import('eslint').Linter.Config} */
2 | // eslint-disable-next-line no-undef
3 | module.exports = {
4 | root: true,
5 | parser: '@typescript-eslint/parser',
6 | plugins: [
7 | '@typescript-eslint',
8 | ],
9 | extends: [
10 | 'eslint:recommended',
11 | 'plugin:@typescript-eslint/recommended',
12 | ],
13 | rules: {
14 | 'semi': [2, "always"],
15 | '@typescript-eslint/no-unused-vars': 0,
16 | '@typescript-eslint/no-explicit-any': 0,
17 | '@typescript-eslint/explicit-module-boundary-types': 0,
18 | '@typescript-eslint/no-non-null-assertion': 0,
19 | },
20 | ignorePatterns: ["src/webviews"],
21 | env: ["node"]
22 | };
--------------------------------------------------------------------------------
/.github/workflows/dispatch.yml:
--------------------------------------------------------------------------------
1 | name: Repo Events Repository Dispatch
2 |
3 | on:
4 | - issues
5 | - issue_comment
6 | - pull_request
7 |
8 | jobs:
9 | preflight-job:
10 | name: Dispatch
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Print Outputs
14 | env:
15 | outputs: ${{ toJSON(github) }}
16 | run: |
17 | echo outputs: $outputs
18 | - name: Repository Dispatch
19 | uses: peter-evans/repository-dispatch@v1
20 | with:
21 | token: ${{ secrets.PAT }}
22 | repository: githubocto/next-devex-workflows # repo to send event to
23 | event-type: repoevents # name of the custom event
24 | client-payload: '{"event": ${{ toJSON(github) }}}'
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | out
2 | node_modules
3 | .vscode-test/
4 | *.vsix
5 | .snowpack
6 | yarn-error.log
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "semi": false,
4 | "singleQuote": true,
5 | "overrides": [
6 | {
7 | "files": "tailwind.config.js",
8 | "options": {
9 | "quoteProps": "consistent",
10 | "printWidth": 200
11 | }
12 | }
13 | ]
14 | }
--------------------------------------------------------------------------------
/.vscode/css_custom_data.json:
--------------------------------------------------------------------------------
1 | {
2 | "atDirectives": [
3 | {
4 | "name": "@tailwind",
5 | "description": "Use the @tailwind directive to insert Tailwind’s `base`, `components`, `utilities`, and `screens` styles into your CSS.",
6 | "references": [
7 | {
8 | "name": "Tailwind’s “Functions & Directives” documentation",
9 | "url": "https://tailwindcss.com/docs/functions-and-directives/#tailwind"
10 | }
11 | ]
12 | },
13 | {
14 | "name": "@screen",
15 | "description": "Use the @screen directive to apply Tailwind's responsive breakpoints",
16 | "references": [
17 | {
18 | "name": "Tailwind’s “Functions & Directives” documentation",
19 | "url": "https://tailwindcss.com/docs/functions-and-directives/#screen"
20 | }
21 | ]
22 | },
23 | {
24 | "name": "@layer",
25 | "description": "Use the @layer directive to tell Tailwind which \"bucket\" a set of custom styles belong to.",
26 | "references": [
27 | {
28 | "name": "Tailwind’s “Functions & Directives” documentation",
29 | "url": "https://tailwindcss.com/docs/functions-and-directives/#layer"
30 | }
31 | ]
32 | }
33 | ]
34 | }
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
4 |
5 | // List of extensions which should be recommended for users of this workspace.
6 | "recommendations": [
7 | "dbaeumer.vscode-eslint",
8 | "csstools.postcss",
9 | "esbenp.prettier-vscode",
10 | "bradlc.vscode-tailwindcss"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [{
8 | "name": "Run Extension",
9 | "type": "extensionHost",
10 | "request": "launch",
11 | "runtimeExecutable": "${execPath}",
12 | "args": [
13 | "--extensionDevelopmentPath=${workspaceFolder}"
14 | ],
15 | "outFiles": [
16 | "${workspaceFolder}/out/**/*.js"
17 | ],
18 | "preLaunchTask": "npm: watch"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "css.customData": [".vscode/css_custom_data.json"]
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "script": "build",
8 | "label": "snowpack-build",
9 | "type": "npm"
10 | },
11 | {
12 | "script": "dev",
13 | "label": "snowpack-build-dev",
14 | "type": "npm",
15 | "problemMatcher": {
16 | "owner": "typescript",
17 | "fileLocation": "absolute",
18 | "pattern": {
19 | "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
20 | "file": 1,
21 | "line": 2,
22 | "column": 3,
23 | "severity": 4,
24 | "message": 5
25 | },
26 | "background": {
27 | "activeOnStart": false,
28 | "beginsPattern": "^\\[nodemon\\] starting",
29 | "endsPattern": "^\\[nodemon\\] clean exit"
30 | }
31 | },
32 | "isBackground": true
33 | },
34 | {
35 | "type": "npm",
36 | "script": "watch",
37 | "problemMatcher": "$tsc-watch",
38 | "isBackground": true,
39 | "presentation": {
40 | "reveal": "never"
41 | },
42 | "group": {
43 | "kind": "build",
44 | "isDefault": true
45 | },
46 | "dependsOn": ["snowpack-build-dev"]
47 | }
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | .vscode-test/**
3 | src/**
4 | .gitignore
5 | .yarnrc
6 | vsc-extension-quickstart.md
7 | **/tsconfig.json
8 | **/.eslintrc.json
9 | **/*.map
10 | **/*.ts
11 | node_modules
12 | !node_modules/vscode-codicons/dist/codicon.css
13 | !node_modules/vscode-codicons/dist/codicon.ttf
14 | .parcel-cache
15 | tailwind.config.json
16 | webpack.config.js
17 | screenshots
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 GitHub Next
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flat Editor VSCode Extension
2 |
3 |
4 |
5 |
6 |
7 | 👉🏽 👉🏽 👉🏽 **Full writeup**: [Flat Data Project](https://octo.github.com/projects/flat-data) 👈🏽 👈🏽 👈🏽
8 |
9 | Flat Editor is a VSCode extension that steps you through the process of creating a [Flat Data Action](https://github.com/githubocto/flat), which makes it easy to fetch data and commit it to your repository.
10 |
11 | Flat Data is a GitHub action which makes it easy to fetch data and commit it to your repository as flatfiles. The action is intended to be run on a schedule, retrieving data from any supported target and creating a commit if there is any change to the fetched data. Flat Data builds on the [“git scraping” approach pioneered by Simon Willison](https://simonwillison.net/2020/Oct/9/git-scraping/) to offer a simple pattern for bringing working datasets into your repositories and versioning them, because developing against local datasets is faster and easier than working with data over the wire.
12 |
13 | ## Usage
14 |
15 | ### VSCode
16 |
17 | To use Flat Editor, first [install the extension](https://marketplace.visualstudio.com/items?itemName=githubocto.flat).
18 |
19 | If you're starting from an empty repository, invoke the VSCode Command Palette via the shortcut Cmd+Shift+P and select **Initialize Flat YML File**
20 |
21 | 
22 |
23 | This will generate a `flat.yml` file in the `.github/workflows` directory, and will open a GUI through which you can configure your Flat action.
24 |
25 | 
26 |
27 | At any given time, you can view the raw content of the underlying YML file via the **View the raw YAML** button in the GUI, or via the following button at the top right of your VSCode workspace.
28 |
29 | 
30 |
31 | Changes to `flat.yml` are saved automatically when using the GUI, but feel free to save manually via Cmd+S if the habit is as deeply engrained for you as it is for us 😀
32 |
33 | ## Action Configuration
34 |
35 | Currently, Flat supports the ingestion of data via the two following sources:
36 |
37 | 1. Publicly accessible HTTP endpoint
38 | 2. SQL Database (accessible via connection string)
39 |
40 | Flat assumes that you'd like to run your workflow on a given schedule, and to that end exposes a GUI for specifying a CRON job as part of the action definition. We've selected a handful of default values, but feel free to enter any valid CRON string here. We'll even validate the CRON for you as you type!
41 |
42 | 
43 |
44 | ### Creating an HTTP action
45 |
46 | 
47 |
48 | To create an HTTP action, you'll be asked for the following inputs:
49 |
50 | 1. A result filename (the filename and extension that your results will be written to, e.g., `vaccination-data.json`).
51 | 2. A publicly accessible URL (we'll try to render a helpful inline preview of the response from this endpoint if we can)
52 | 3. An optional path to a [postprocessing script](https://github.com/githubocto/flat#postprocessing), if you wish to perform further transformation or work on the fetched date
53 |
54 | ### Creating a SQL action
55 |
56 | 
57 |
58 | To create a SQL action, you'll be asked for the following inputs:
59 |
60 | 1. A result filename (the filename and extension that your results will be written to, e.g., `vaccination-data.json`).
61 | 2. A path to a SQL query
62 | 3. A database connection string \*
63 | 4. An optional path to a [postprocessing script](https://github.com/githubocto/flat#postprocessing), if you wish to perform further transformation or work on the fetched date
64 |
65 | \* Note that we will encrypt this value and create a [GitHub secret](https://docs.github.com/en/actions/reference/encrypted-secrets) in your repository for this connection string. No sensitive data will be committed to your repository. Keep in mind that your repository must have an upstream remote on github.com in order for us to create the secret.
66 |
67 | ## Running Your Action
68 |
69 | After you've added the requisite steps to your Flat action, push your changes to your GitHub repository. Your workflow should run automatically. Additionally, under the hood, the extension lists your optional postprocessing and/or SQL query files as workflow triggers, meaning the workflow will run anytime these files change. You can run your workflows manually, too, thanks to the `workflow_dispatch: {}` value that the extension adds to your Flat action.
70 |
71 | ```yaml
72 | workflow_dispatch: {}
73 | push:
74 | paths:
75 | - .github/workflows/flat.yml
76 | - ./rearrange-vax-data.ts
77 | ```
78 |
79 | ## Development and Deployment
80 |
81 | Deploy a new version with:
82 |
83 | First make sure you're a part of the githubocto marketplace team [here](https://marketplace.visualstudio.com/manage/publishers/githubocto).
84 |
85 |
86 | 1. Get a PAT [here](https://dev.azure.com/githubocto/_usersSettings/tokens) (first time)
87 | 2. `vsce login githubocto` (first time)
88 | 3. `vsce publish [minor|major|patch]` This will create a new version and update package.json accordingly.
89 | 4. `git push` the change to `package.json`
90 |
91 | ## Issues
92 |
93 | If you run into any trouble or have questions, feel free to [open an issue](https://github.com/githubocto/flat-editor/issues). Sharing your `flat.yml` with us in the issue will help us understand what might be happening.
94 |
95 | ❤️ GitHub OCTO
96 |
97 | ## License
98 |
99 | [MIT](LICENSE)
100 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/githubocto/flat-editor/7cf54f56c4bdda31e79257e16cc268b4415ebff4/demo.gif
--------------------------------------------------------------------------------
/docs/icon-transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/githubocto/flat-editor/7cf54f56c4bdda31e79257e16cc268b4415ebff4/docs/icon-transparent.png
--------------------------------------------------------------------------------
/docs/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/githubocto/flat-editor/7cf54f56c4bdda31e79257e16cc268b4415ebff4/docs/icon.png
--------------------------------------------------------------------------------
/git.d.ts:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | import { Uri, Event, Disposable, ProviderResult, Command } from 'vscode'
7 | export { ProviderResult } from 'vscode'
8 |
9 | export interface Git {
10 | readonly path: string
11 | }
12 |
13 | export interface InputBox {
14 | value: string
15 | }
16 |
17 | export const enum ForcePushMode {
18 | Force,
19 | ForceWithLease,
20 | }
21 |
22 | export const enum RefType {
23 | Head,
24 | RemoteHead,
25 | Tag,
26 | }
27 |
28 | export interface Ref {
29 | readonly type: RefType
30 | readonly name?: string
31 | readonly commit?: string
32 | readonly remote?: string
33 | }
34 |
35 | export interface UpstreamRef {
36 | readonly remote: string
37 | readonly name: string
38 | }
39 |
40 | export interface Branch extends Ref {
41 | readonly upstream?: UpstreamRef
42 | readonly ahead?: number
43 | readonly behind?: number
44 | }
45 |
46 | export interface Commit {
47 | readonly hash: string
48 | readonly message: string
49 | readonly parents: string[]
50 | readonly authorDate?: Date
51 | readonly authorName?: string
52 | readonly authorEmail?: string
53 | readonly commitDate?: Date
54 | }
55 |
56 | export interface Submodule {
57 | readonly name: string
58 | readonly path: string
59 | readonly url: string
60 | }
61 |
62 | export interface Remote {
63 | readonly name: string
64 | readonly fetchUrl?: string
65 | readonly pushUrl?: string
66 | readonly isReadOnly: boolean
67 | }
68 |
69 | export const enum Status {
70 | INDEX_MODIFIED,
71 | INDEX_ADDED,
72 | INDEX_DELETED,
73 | INDEX_RENAMED,
74 | INDEX_COPIED,
75 |
76 | MODIFIED,
77 | DELETED,
78 | UNTRACKED,
79 | IGNORED,
80 | INTENT_TO_ADD,
81 |
82 | ADDED_BY_US,
83 | ADDED_BY_THEM,
84 | DELETED_BY_US,
85 | DELETED_BY_THEM,
86 | BOTH_ADDED,
87 | BOTH_DELETED,
88 | BOTH_MODIFIED,
89 | }
90 |
91 | export interface Change {
92 | /**
93 | * Returns either `originalUri` or `renameUri`, depending
94 | * on whether this change is a rename change. When
95 | * in doubt always use `uri` over the other two alternatives.
96 | */
97 | readonly uri: Uri
98 | readonly originalUri: Uri
99 | readonly renameUri: Uri | undefined
100 | readonly status: Status
101 | }
102 |
103 | export interface RepositoryState {
104 | readonly HEAD: Branch | undefined
105 | readonly refs: Ref[]
106 | readonly remotes: Remote[]
107 | readonly submodules: Submodule[]
108 | readonly rebaseCommit: Commit | undefined
109 |
110 | readonly mergeChanges: Change[]
111 | readonly indexChanges: Change[]
112 | readonly workingTreeChanges: Change[]
113 |
114 | readonly onDidChange: Event
115 | }
116 |
117 | export interface RepositoryUIState {
118 | readonly selected: boolean
119 | readonly onDidChange: Event
120 | }
121 |
122 | /**
123 | * Log options.
124 | */
125 | export interface LogOptions {
126 | /** Max number of log entries to retrieve. If not specified, the default is 32. */
127 | readonly maxEntries?: number
128 | readonly path?: string
129 | }
130 |
131 | export interface CommitOptions {
132 | all?: boolean | 'tracked'
133 | amend?: boolean
134 | signoff?: boolean
135 | signCommit?: boolean
136 | empty?: boolean
137 | noVerify?: boolean
138 | requireUserConfig?: boolean
139 | useEditor?: boolean
140 | verbose?: boolean
141 | postCommitCommand?: string
142 | }
143 |
144 | export interface FetchOptions {
145 | remote?: string
146 | ref?: string
147 | all?: boolean
148 | prune?: boolean
149 | depth?: number
150 | }
151 |
152 | export interface BranchQuery {
153 | readonly remote?: boolean
154 | readonly pattern?: string
155 | readonly count?: number
156 | readonly contains?: string
157 | }
158 |
159 | export interface Repository {
160 | readonly rootUri: Uri
161 | readonly inputBox: InputBox
162 | readonly state: RepositoryState
163 | readonly ui: RepositoryUIState
164 |
165 | getConfigs(): Promise<{ key: string; value: string }[]>
166 | getConfig(key: string): Promise
167 | setConfig(key: string, value: string): Promise
168 | getGlobalConfig(key: string): Promise
169 |
170 | getObjectDetails(
171 | treeish: string,
172 | path: string
173 | ): Promise<{ mode: string; object: string; size: number }>
174 | detectObjectType(
175 | object: string
176 | ): Promise<{ mimetype: string; encoding?: string }>
177 | buffer(ref: string, path: string): Promise
178 | show(ref: string, path: string): Promise
179 | getCommit(ref: string): Promise
180 |
181 | add(paths: string[]): Promise
182 | revert(paths: string[]): Promise
183 | clean(paths: string[]): Promise
184 |
185 | apply(patch: string, reverse?: boolean): Promise
186 | diff(cached?: boolean): Promise
187 | diffWithHEAD(): Promise
188 | diffWithHEAD(path: string): Promise
189 | diffWith(ref: string): Promise
190 | diffWith(ref: string, path: string): Promise
191 | diffIndexWithHEAD(): Promise
192 | diffIndexWithHEAD(path: string): Promise
193 | diffIndexWith(ref: string): Promise
194 | diffIndexWith(ref: string, path: string): Promise
195 | diffBlobs(object1: string, object2: string): Promise
196 | diffBetween(ref1: string, ref2: string): Promise
197 | diffBetween(ref1: string, ref2: string, path: string): Promise
198 |
199 | hashObject(data: string): Promise
200 |
201 | createBranch(name: string, checkout: boolean, ref?: string): Promise
202 | deleteBranch(name: string, force?: boolean): Promise
203 | getBranch(name: string): Promise
204 | getBranches(query: BranchQuery): Promise[
205 | setBranchUpstream(name: string, upstream: string): Promise
206 |
207 | getMergeBase(ref1: string, ref2: string): Promise
208 |
209 | tag(name: string, upstream: string): Promise
210 | deleteTag(name: string): Promise
211 |
212 | status(): Promise
213 | checkout(treeish: string): Promise
214 |
215 | addRemote(name: string, url: string): Promise
216 | removeRemote(name: string): Promise
217 | renameRemote(name: string, newName: string): Promise
218 |
219 | fetch(options?: FetchOptions): Promise
220 | fetch(remote?: string, ref?: string, depth?: number): Promise
221 | pull(unshallow?: boolean): Promise
222 | push(
223 | remoteName?: string,
224 | branchName?: string,
225 | setUpstream?: boolean,
226 | force?: ForcePushMode
227 | ): Promise
228 |
229 | blame(path: string): Promise
230 | log(options?: LogOptions): Promise
231 |
232 | commit(message: string, opts?: CommitOptions): Promise
233 | }
234 |
235 | export interface RemoteSource {
236 | readonly name: string
237 | readonly description?: string
238 | readonly url: string | string[]
239 | }
240 |
241 | export interface RemoteSourceProvider {
242 | readonly name: string
243 | readonly icon?: string // codicon name
244 | readonly supportsQuery?: boolean
245 | getRemoteSources(query?: string): ProviderResult
246 | getBranches?(url: string): ProviderResult
247 | publishRepository?(repository: Repository): Promise
248 | }
249 |
250 | export interface RemoteSourcePublisher {
251 | readonly name: string
252 | readonly icon?: string // codicon name
253 | publishRepository(repository: Repository): Promise
254 | }
255 |
256 | export interface Credentials {
257 | readonly username: string
258 | readonly password: string
259 | }
260 |
261 | export interface CredentialsProvider {
262 | getCredentials(host: Uri): ProviderResult
263 | }
264 |
265 | export type CommitCommand = Command & { description?: string }
266 |
267 | export interface PostCommitCommandsProvider {
268 | getCommands(repository: Repository): CommitCommand[]
269 | }
270 |
271 | export interface PushErrorHandler {
272 | handlePushError(
273 | repository: Repository,
274 | remote: Remote,
275 | refspec: string,
276 | error: Error & { gitErrorCode: GitErrorCodes }
277 | ): Promise
278 | }
279 |
280 | export type APIState = 'uninitialized' | 'initialized'
281 |
282 | export interface PublishEvent {
283 | repository: Repository
284 | branch?: string
285 | }
286 |
287 | export interface API {
288 | readonly state: APIState
289 | readonly onDidChangeState: Event
290 | readonly onDidPublish: Event
291 | readonly git: Git
292 | readonly repositories: Repository[]
293 | readonly onDidOpenRepository: Event
294 | readonly onDidCloseRepository: Event
295 |
296 | toGitUri(uri: Uri, ref: string): Uri
297 | getRepository(uri: Uri): Repository | null
298 | init(root: Uri): Promise
299 | openRepository(root: Uri): Promise
300 |
301 | registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable
302 | registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable
303 | registerCredentialsProvider(provider: CredentialsProvider): Disposable
304 | registerPostCommitCommandsProvider(
305 | provider: PostCommitCommandsProvider
306 | ): Disposable
307 | registerPushErrorHandler(handler: PushErrorHandler): Disposable
308 | }
309 |
310 | export interface GitExtension {
311 | readonly enabled: boolean
312 | readonly onDidChangeEnablement: Event
313 |
314 | /**
315 | * Returns a specific API version.
316 | *
317 | * Throws error if git extension is disabled. You can listen to the
318 | * [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event
319 | * to know when the extension becomes enabled/disabled.
320 | *
321 | * @param version Version number.
322 | * @returns API instance
323 | */
324 | getAPI(version: 1): API
325 | }
326 |
327 | export const enum GitErrorCodes {
328 | BadConfigFile = 'BadConfigFile',
329 | AuthenticationFailed = 'AuthenticationFailed',
330 | NoUserNameConfigured = 'NoUserNameConfigured',
331 | NoUserEmailConfigured = 'NoUserEmailConfigured',
332 | NoRemoteRepositorySpecified = 'NoRemoteRepositorySpecified',
333 | NotAGitRepository = 'NotAGitRepository',
334 | NotAtRepositoryRoot = 'NotAtRepositoryRoot',
335 | Conflict = 'Conflict',
336 | StashConflict = 'StashConflict',
337 | UnmergedChanges = 'UnmergedChanges',
338 | PushRejected = 'PushRejected',
339 | RemoteConnectionError = 'RemoteConnectionError',
340 | DirtyWorkTree = 'DirtyWorkTree',
341 | CantOpenResource = 'CantOpenResource',
342 | GitNotFound = 'GitNotFound',
343 | CantCreatePipe = 'CantCreatePipe',
344 | PermissionDenied = 'PermissionDenied',
345 | CantAccessRemote = 'CantAccessRemote',
346 | RepositoryNotFound = 'RepositoryNotFound',
347 | RepositoryIsLocked = 'RepositoryIsLocked',
348 | BranchNotFullyMerged = 'BranchNotFullyMerged',
349 | NoRemoteReference = 'NoRemoteReference',
350 | InvalidBranchName = 'InvalidBranchName',
351 | BranchAlreadyExists = 'BranchAlreadyExists',
352 | NoLocalChanges = 'NoLocalChanges',
353 | NoStashFound = 'NoStashFound',
354 | LocalChangesOverwritten = 'LocalChangesOverwritten',
355 | NoUpstreamBranch = 'NoUpstreamBranch',
356 | IsInSubmodule = 'IsInSubmodule',
357 | WrongCase = 'WrongCase',
358 | CantLockRef = 'CantLockRef',
359 | CantRebaseMultipleBranches = 'CantRebaseMultipleBranches',
360 | PatchDoesNotApply = 'PatchDoesNotApply',
361 | NoPathFound = 'NoPathFound',
362 | UnknownPath = 'UnknownPath',
363 | EmptyCommitMessage = 'EmptyCommitMessage',
364 | }
365 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flat",
3 | "displayName": "Flat Editor",
4 | "description": "An editor for flat-data configurations",
5 | "version": "0.24.0",
6 | "publisher": "githubocto",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/githubocto/flat-editor"
10 | },
11 | "bugs": {
12 | "url": "https://github.com/githubocto/flat-editor/issues",
13 | "email": "idan@github.com"
14 | },
15 | "engines": {
16 | "vscode": "^1.54.0"
17 | },
18 | "icon": "docs/icon-transparent.png",
19 | "categories": [
20 | "Other"
21 | ],
22 | "activationEvents": [
23 | "onLanguage:yaml",
24 | "onCustomEditor:flat.config",
25 | "onCommand:flat.initializeFlatYml"
26 | ],
27 | "babel": {
28 | "presets": [
29 | "@babel/preset-env",
30 | "@babel/preset-typescript"
31 | ],
32 | "plugins": [
33 | "@babel/plugin-proposal-class-properties",
34 | "@babel/plugin-transform-runtime"
35 | ]
36 | },
37 | "extensionDependencies": [
38 | "vscode.git"
39 | ],
40 | "main": "./out/extension.js",
41 | "contributes": {
42 | "customEditors": [
43 | {
44 | "viewType": "flat.config",
45 | "displayName": "Flat Config",
46 | "selector": [
47 | {
48 | "filenamePattern": "**/.github/workflows/flat.yml"
49 | }
50 | ],
51 | "priority": "default"
52 | }
53 | ],
54 | "commands": [
55 | {
56 | "command": "flat.showPreview",
57 | "title": "Show Editor",
58 | "category": "Flat Editor",
59 | "icon": "$(go-to-file)"
60 | },
61 | {
62 | "command": "flat.showRaw",
63 | "title": "Show Raw YAML File",
64 | "category": "Flat Editor",
65 | "icon": "$(go-to-file)"
66 | },
67 | {
68 | "command": "flat.showPreviewToSide",
69 | "title": "Show Editor to the Side",
70 | "category": "Flat Editor",
71 | "icon": "$(go-to-file)"
72 | },
73 | {
74 | "command": "flat.showRawToSide",
75 | "title": "Show Raw YAML File to the Side",
76 | "category": "Flat Editor",
77 | "icon": "$(go-to-file)"
78 | },
79 | {
80 | "command": "flat.initializeFlatYml",
81 | "title": "Initialize Flat YML File",
82 | "category": "Flat Editor"
83 | }
84 | ],
85 | "menus": {
86 | "editor/title": [
87 | {
88 | "command": "flat.showPreview",
89 | "alt": "flat.showPreviewToSide",
90 | "group": "navigation",
91 | "when": "resourcePath =~ /.github/workflows/flat.yml$/ && editorLangId == yaml && activeEditor == workbench.editors.files.textFileEditor"
92 | },
93 | {
94 | "command": "flat.showRaw",
95 | "alt": "flat.showRawToSide",
96 | "group": "navigation",
97 | "when": "resourcePath =~ /.github/workflows/flat.yml$/ && activeEditor == WebviewEditor"
98 | }
99 | ]
100 | }
101 | },
102 | "scripts": {
103 | "vscode:prepublish": "run-s clean compile",
104 | "clean": "rm -rf out dist",
105 | "compile": "run-s webpack:build webview:build",
106 | "lint": "eslint . --ext .ts,.tsx",
107 | "watch": "tsc -watch -p ./",
108 | "format": "prettier --write **/*.ts",
109 | "dev": "nodemon --watch src/webviews/ -e 'ts,tsx,css' --exec snowpack build",
110 | "build": "TAILWIND_MODE='build' snowpack build",
111 | "webpack:build": "webpack",
112 | "webview:build": "TAILWIND_MODE='build' snowpack build"
113 | },
114 | "devDependencies": {
115 | "@babel/core": "^7.13.14",
116 | "@babel/plugin-proposal-class-properties": "^7.13.0",
117 | "@babel/plugin-transform-runtime": "^7.13.10",
118 | "@babel/preset-env": "^7.13.12",
119 | "@babel/preset-typescript": "^7.13.0",
120 | "@snowpack/plugin-dotenv": "^2.0.5",
121 | "@snowpack/plugin-postcss": "^1.2.1",
122 | "@snowpack/plugin-typescript": "^1.2.1",
123 | "@types/lodash-es": "^4.17.4",
124 | "@types/node": "^12.12.0",
125 | "@types/react": "^17.0.3",
126 | "@types/react-dom": "^17.0.2",
127 | "@types/snowpack-env": "^2.3.3",
128 | "@types/tailwindcss": "^2.0.2",
129 | "@types/vscode": "^1.54.0",
130 | "@typescript-eslint/eslint-plugin": "^4.16.0",
131 | "@typescript-eslint/parser": "^4.16.0",
132 | "autoprefixer": "^10.2.5",
133 | "babel-loader": "^8.2.2",
134 | "eslint": "^7.21.0",
135 | "nodemon": "^2.0.7",
136 | "npm-run-all": "^4.1.5",
137 | "postcss": "^8.4.5",
138 | "prettier": "^2.2.1",
139 | "snowpack": "^3.1.0",
140 | "tailwindcss": "^3.0.17",
141 | "ts-loader": "^8.1.0",
142 | "typescript": "^4.2.2",
143 | "webpack": "^5.30.0",
144 | "webpack-cli": "^4.6.0"
145 | },
146 | "dependencies": {
147 | "@octokit/rest": "^18.5.2",
148 | "@radix-ui/react-dropdown-menu": "^0.0.17",
149 | "@reach/combobox": "^0.16.5",
150 | "@types/isomorphic-fetch": "^0.0.35",
151 | "@vscode/codicons": "^0.0.27",
152 | "@vscode/webview-ui-toolkit": "^0.9.0",
153 | "cronstrue": "^1.123.0",
154 | "fast-glob": "^3.2.5",
155 | "git-url-parse": "^11.4.4",
156 | "immer": "^9.0.1",
157 | "isomorphic-fetch": "^3.0.0",
158 | "lodash-es": "^4.17.21",
159 | "nanoid": "^3.1.22",
160 | "react": "^17.0.1",
161 | "react-dom": "^17.0.1",
162 | "react-error-boundary": "^3.1.4",
163 | "ts-debounce": "^3.0.0",
164 | "tweetsodium": "0.0.4",
165 | "yaml": "^2.0.0-4",
166 | "yup": "^0.32.9",
167 | "zod": "^3.0.0-alpha.33",
168 | "zustand": "^3.3.3"
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/screenshots/command-panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/githubocto/flat-editor/7cf54f56c4bdda31e79257e16cc268b4415ebff4/screenshots/command-panel.png
--------------------------------------------------------------------------------
/screenshots/cron-editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/githubocto/flat-editor/7cf54f56c4bdda31e79257e16cc268b4415ebff4/screenshots/cron-editor.png
--------------------------------------------------------------------------------
/screenshots/gui-view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/githubocto/flat-editor/7cf54f56c4bdda31e79257e16cc268b4415ebff4/screenshots/gui-view.png
--------------------------------------------------------------------------------
/screenshots/http-step.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/githubocto/flat-editor/7cf54f56c4bdda31e79257e16cc268b4415ebff4/screenshots/http-step.png
--------------------------------------------------------------------------------
/screenshots/show-preview-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/githubocto/flat-editor/7cf54f56c4bdda31e79257e16cc268b4415ebff4/screenshots/show-preview-button.png
--------------------------------------------------------------------------------
/screenshots/sql-step.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/githubocto/flat-editor/7cf54f56c4bdda31e79257e16cc268b4415ebff4/screenshots/sql-step.png
--------------------------------------------------------------------------------
/snowpack.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import("snowpack").SnowpackUserConfig } */
2 | module.exports = {
3 | mount: {
4 | 'src/webviews/public': { url: '/', static: true },
5 | 'src/webviews/src': { url: '/' },
6 | 'node_modules/@vscode/codicons/dist': { url: '/public', static: true },
7 | },
8 | plugins: [
9 | '@snowpack/plugin-dotenv',
10 | '@snowpack/plugin-postcss',
11 | [
12 | '@snowpack/plugin-typescript',
13 | { args: '--project ./tsconfig-webview.json' },
14 | ],
15 | ],
16 | routes: [
17 | /* Enable an SPA Fallback in development: */
18 | // {"match": "routes", "src": ".*", "dest": "/index.html"},
19 | ],
20 | optimize: {
21 | bundle: true,
22 | minify: true,
23 | target: 'es2020',
24 | entrypoints: ['index.js'], // one per webview
25 | },
26 | packageOptions: {},
27 | devOptions: {
28 | output: 'stream',
29 | },
30 | buildOptions: {
31 | out: 'out/webviews',
32 | },
33 | }
34 |
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import * as path from 'path'
3 | import * as fs from 'fs'
4 | import { stringify } from 'yaml'
5 |
6 | import { FlatConfigEditor } from './flatConfigEditor'
7 |
8 | export async function activate(context: vscode.ExtensionContext) {
9 | const editor = FlatConfigEditor.register(context)
10 | context.subscriptions.push(editor)
11 |
12 | const showEditor =
13 | ({ isPreview = false, onSide = false }) =>
14 | () => {
15 | const workspaceRootUri = vscode.workspace.workspaceFolders?.[0].uri
16 | if (!workspaceRootUri) return
17 | const flatFileUri = vscode.Uri.joinPath(
18 | workspaceRootUri,
19 | '.github/workflows',
20 | 'flat.yml'
21 | )
22 |
23 | vscode.commands.executeCommand(
24 | 'vscode.openWith',
25 | flatFileUri,
26 | isPreview ? 'flat.config' : 'default',
27 | onSide ? { viewColumn: vscode.ViewColumn.Beside, preview: false } : {}
28 | )
29 | }
30 |
31 | context.subscriptions.push(
32 | vscode.commands.registerCommand(
33 | 'flat.showPreview',
34 | showEditor({ isPreview: true, onSide: false })
35 | )
36 | )
37 | context.subscriptions.push(
38 | vscode.commands.registerCommand(
39 | 'flat.showRaw',
40 | showEditor({ isPreview: false, onSide: false })
41 | )
42 | )
43 | context.subscriptions.push(
44 | vscode.commands.registerCommand(
45 | 'flat.showPreviewToSide',
46 | showEditor({ isPreview: true, onSide: true })
47 | )
48 | )
49 | context.subscriptions.push(
50 | vscode.commands.registerCommand(
51 | 'flat.showRawToSide',
52 | showEditor({ isPreview: false, onSide: true })
53 | )
54 | )
55 |
56 | context.subscriptions.push(
57 | vscode.commands.registerCommand('flat.initializeFlatYml', async () => {
58 | let rootPath: vscode.WorkspaceFolder
59 | const folders = vscode.workspace.workspaceFolders
60 |
61 | if (!folders) {
62 | return
63 | }
64 | rootPath = folders[0]
65 |
66 | const workflowsDir = path.join(rootPath.uri.fsPath, '.github/workflows')
67 | const flatYmlPath = path.join(workflowsDir, 'flat.yml')
68 |
69 | if (fs.existsSync(flatYmlPath)) {
70 | showEditor({ isPreview: true })()
71 | return
72 | }
73 |
74 | fs.mkdirSync(workflowsDir, { recursive: true })
75 |
76 | const flatStub = {
77 | name: 'data',
78 | on: {
79 | schedule: [{ cron: '0 0 * * *' }],
80 | workflow_dispatch: {},
81 | push: {
82 | paths: ['.github/workflows/flat.yml'],
83 | },
84 | },
85 | jobs: {
86 | scheduled: {
87 | 'runs-on': 'ubuntu-latest',
88 | steps: [
89 | {
90 | name: 'Setup deno',
91 | uses: 'denoland/setup-deno@main',
92 | with: {
93 | 'deno-version': 'v1.10.x',
94 | },
95 | },
96 | {
97 | name: 'Check out repo',
98 | uses: 'actions/checkout@v2',
99 | },
100 | ],
101 | },
102 | },
103 | }
104 |
105 | fs.writeFileSync(path.join(workflowsDir, 'flat.yml'), stringify(flatStub))
106 | showEditor({ isPreview: true })()
107 | })
108 | )
109 | }
110 |
--------------------------------------------------------------------------------
/src/flatConfigEditor.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import * as fg from 'fast-glob'
3 | import * as fetch from 'isomorphic-fetch'
4 | import { parse, stringify } from 'yaml'
5 | import { debounce } from 'ts-debounce'
6 |
7 | import { Octokit } from '@octokit/rest'
8 | import { VSCodeGit } from './git'
9 | import { getNonce, getSession } from './lib'
10 | import type { FlatState } from './types'
11 |
12 | const sodium = require('tweetsodium')
13 |
14 | export class FlatConfigEditor implements vscode.CustomTextEditorProvider {
15 | public static register(context: vscode.ExtensionContext): vscode.Disposable {
16 | const provider = new FlatConfigEditor(context)
17 | const providerRegistration = vscode.window.registerCustomEditorProvider(
18 | FlatConfigEditor.viewType,
19 | provider
20 | )
21 | return providerRegistration
22 | }
23 |
24 | private static readonly viewType = 'flat.config'
25 |
26 | constructor(private readonly context: vscode.ExtensionContext) {}
27 |
28 | // Called when our custom editor is opened.
29 | public async resolveCustomTextEditor(
30 | document: vscode.TextDocument,
31 | webviewPanel: vscode.WebviewPanel,
32 | _token: vscode.CancellationToken
33 | ): Promise {
34 | const updateWebview = async (document: vscode.TextDocument) => {
35 | if (vscode.window.activeTextEditor) {
36 | webviewPanel.webview.html = await this.getHtmlForWebview(
37 | webviewPanel.webview
38 | )
39 | } else {
40 | const rawFlatYaml = document.getText()
41 | const parsedConfig = parse(rawFlatYaml)
42 |
43 | webviewPanel.webview.postMessage({
44 | command: 'updateState',
45 | config: parsedConfig,
46 | })
47 | }
48 | }
49 |
50 | const updateWebviewState = async () => {
51 | const rawFlatYaml = document.getText()
52 | const parsedConfig = parse(rawFlatYaml)
53 | webviewPanel.webview.postMessage({
54 | command: 'updateState',
55 | config: parsedConfig,
56 | })
57 | }
58 |
59 | const changeDocumentSubscription = vscode.workspace.onDidSaveTextDocument(
60 | e => {
61 | if (e.uri.toString() === document.uri.toString()) {
62 | updateWebview(e)
63 | }
64 | }
65 | )
66 |
67 | // Make sure we get rid of the listener when our editor is closed.
68 | webviewPanel.onDidDispose(() => {
69 | changeDocumentSubscription.dispose()
70 | })
71 |
72 | // Setup initial content for the webview
73 | webviewPanel.webview.options = {
74 | enableScripts: true,
75 | }
76 |
77 | try {
78 | webviewPanel.webview.html = await this.getHtmlForWebview(
79 | webviewPanel.webview
80 | )
81 | } catch (e) {
82 | await vscode.window.showErrorMessage(
83 | "Please make sure you're in a repository with a valid upstream GitHub remote"
84 | )
85 | await vscode.commands.executeCommand(
86 | 'workbench.action.revertAndCloseActiveEditor'
87 | )
88 | // For whatever reason, this doesn't close the webview.
89 | await webviewPanel.dispose()
90 | }
91 |
92 | // Receive message from the webview.
93 | webviewPanel.webview.onDidReceiveMessage(async e => {
94 | switch (e.type) {
95 | case 'openEditor':
96 | this.showEditor(e.data)
97 | break
98 | case 'updateText':
99 | this.updateTextDocument(document, e.data)
100 | break
101 | case 'storeSecret':
102 | this.storeSecret(webviewPanel, e.data)
103 | break
104 | case 'refreshFiles':
105 | this.loadFiles(webviewPanel)
106 | break
107 | case 'getFileContents':
108 | this.loadFileContents(webviewPanel, e.data)
109 | break
110 | case 'refreshState':
111 | updateWebviewState()
112 | case 'getUrlContents':
113 | this.loadUrlContents(webviewPanel, e.data)
114 | break
115 | case 'refreshGitDetails':
116 | webviewPanel.webview.html = await this.getHtmlForWebview(
117 | webviewPanel.webview
118 | )
119 | break
120 | case 'previewFile':
121 | const workspaceRootUri = vscode.workspace.workspaceFolders?.[0].uri
122 | if (!workspaceRootUri) {
123 | return
124 | }
125 | const uri = vscode.Uri.joinPath(workspaceRootUri, e.data)
126 | const doc = await vscode.workspace.openTextDocument(uri)
127 | vscode.window.showTextDocument(doc)
128 | break
129 | default:
130 | break
131 | }
132 | })
133 | }
134 |
135 | /**
136 | * Get the static html used for the editor webviews.
137 | */
138 | private async getHtmlForWebview(webview: vscode.Webview): Promise {
139 | // Local path to script and css for the webview
140 | const scriptUri = webview.asWebviewUri(
141 | vscode.Uri.joinPath(this.context.extensionUri, 'out/webviews/index.js')
142 | )
143 |
144 | const styleVSCodeUri = webview.asWebviewUri(
145 | vscode.Uri.joinPath(this.context.extensionUri, 'out/webviews/index.css')
146 | )
147 | const codiconsUri = webview.asWebviewUri(
148 | vscode.Uri.joinPath(
149 | this.context.extensionUri,
150 | 'out/webviews/public/codicon.css'
151 | )
152 | )
153 |
154 | // Use a nonce to whitelist which scripts can be run
155 | const nonce = getNonce()
156 |
157 | const workspaceRootUri = vscode.workspace.workspaceFolders?.[0].uri
158 | if (!workspaceRootUri) {
159 | throw new Error('No workspace open')
160 | }
161 |
162 | const flatFileUri = vscode.Uri.joinPath(
163 | workspaceRootUri,
164 | '.github/workflows',
165 | 'flat.yml'
166 | )
167 | const document = await vscode.workspace.openTextDocument(flatFileUri)
168 | const rawFlatYaml = document.getText()
169 |
170 | const dirName = workspaceRootUri.path.substring(
171 | workspaceRootUri.path.lastIndexOf('/') + 1
172 | )
173 |
174 | let name,
175 | owner = ''
176 |
177 | try {
178 | const details = await vscode.window.withProgress(
179 | {
180 | location: vscode.ProgressLocation.Notification,
181 | },
182 | async progress => {
183 | progress.report({
184 | message: `Checking for GitHub repository in directory: ${dirName}`,
185 | })
186 |
187 | return await this.getRepoDetails()
188 | }
189 | )
190 | owner = details.owner || ''
191 | name = details.name
192 | } catch (e) {
193 | console.error('Error getting GitHub repository details', e)
194 | }
195 |
196 | const gitRepo = owner && name ? `${owner}/${name}` : ''
197 |
198 | return /* html */ `
199 |
200 |
201 |
202 |
203 |
204 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
218 |
219 | Flat Editor
220 |
221 |
222 |
223 |
224 |
225 | `
226 | }
227 |
228 | private saveDocument(document: vscode.TextDocument) {
229 | document.save()
230 | }
231 |
232 | debouncedSave = debounce(this.saveDocument, 300)
233 |
234 | /**
235 | * Write out the yaml to a given document.
236 | */
237 | private async updateTextDocument(_: vscode.TextDocument, data: any) {
238 | const workspaceRootUri = vscode.workspace.workspaceFolders?.[0].uri
239 | if (!workspaceRootUri) {
240 | throw new Error('No workspace open')
241 | }
242 |
243 | const flatFileUri = vscode.Uri.joinPath(
244 | workspaceRootUri,
245 | '.github/workflows',
246 | 'flat.yml'
247 | )
248 | const document = await vscode.workspace.openTextDocument(flatFileUri)
249 | const currentText = document.getText()
250 |
251 | // todo
252 | const edit = new vscode.WorkspaceEdit()
253 |
254 | const newText = this.serializeWorkflow(data)
255 | if (currentText === newText) return
256 |
257 | // Replaces the entire document every time
258 | // TODO, maybe: more specific edits
259 | edit.replace(
260 | document.uri,
261 | new vscode.Range(0, 0, document.lineCount, 0),
262 | newText
263 | )
264 | await vscode.workspace.applyEdit(edit)
265 | this.debouncedSave(document)
266 | }
267 |
268 | private serializeWorkflow(data: FlatState): string {
269 | // const doc: FlatYamlDoc = {
270 | // name: 'Flat',
271 | // },
272 | // jobs: {},
273 | // }
274 | // if (data.triggerPush) {
275 | // if (data.triggerSchedule) {
276 | // cron: data.triggerSchedule,
277 | // },
278 | // ]
279 | // }
280 |
281 | // data.jobs.forEach(j => {
282 | // doc.jobs[j.name] = {
283 | // {
284 | // name: 'Checkout repo',
285 | // uses: 'actions/checkout@v2',
286 | // },
287 | // ...j.job.steps,
288 | // ],
289 | // }
290 | // })
291 | const serialized = stringify(data)
292 | return serialized
293 | }
294 |
295 | private async storeSecret(
296 | webviewPanel: vscode.WebviewPanel,
297 | data: SecretData
298 | ) {
299 | const { fieldName, value } = data
300 |
301 | let session = await getSession({
302 | createIfNone: true,
303 | })
304 |
305 | // if (!session) {
306 | // session = await getSession({
307 | // createIfNone: true,
308 | // })
309 | // }
310 |
311 | if (!session) return
312 |
313 | const { owner, name } = await this.getRepoDetails()
314 | if (!owner || !name) return
315 |
316 | const octokit = new Octokit({
317 | auth: session.accessToken,
318 | })
319 | // Go time! Let's create a secret for the encrypted conn string.
320 | const keyRes = await octokit.actions.getRepoPublicKey({
321 | owner,
322 | repo: name,
323 | })
324 | const key = keyRes.data.key
325 | // Convert the message and key to Uint8Array's (Buffer implements that interface)
326 | const messageBytes = Buffer.from(value)
327 | const keyBytes = Buffer.from(key, 'base64')
328 | // Encrypt using LibSodium.
329 | const encryptedBytes = sodium.seal(messageBytes, keyBytes)
330 | // Base64 the encrypted secret
331 | const encrypted = Buffer.from(encryptedBytes).toString('base64')
332 | const keyId = keyRes.data.key_id
333 | try {
334 | await octokit.actions.createOrUpdateRepoSecret({
335 | owner: owner,
336 | repo: name,
337 | secret_name: fieldName,
338 | encrypted_value: encrypted,
339 | key_id: keyId,
340 | })
341 |
342 | await webviewPanel.webview.postMessage({
343 | command: 'storeSecretResponse',
344 | fieldName,
345 | status: 'success',
346 | })
347 | } catch (e) {
348 | await vscode.window.showErrorMessage(
349 | "Oh no! We weren't able to create a secret for your connection string."
350 | )
351 | await webviewPanel.webview.postMessage({
352 | command: 'storeSecretResponse',
353 | fieldName,
354 | status: 'error',
355 | })
356 | }
357 | }
358 |
359 | public showEditor = ({
360 | isPreview = false,
361 | onSide = false,
362 | }: ShowEditorOptions): void => {
363 | const workspaceRootUri = vscode.workspace.workspaceFolders?.[0].uri
364 | if (!workspaceRootUri) return
365 | const flatFileUri = vscode.Uri.joinPath(
366 | workspaceRootUri,
367 | '.github/workflows',
368 | 'flat.yml'
369 | )
370 |
371 | vscode.commands.executeCommand(
372 | 'vscode.openWith',
373 | flatFileUri,
374 | isPreview ? 'flat.config' : 'default',
375 | onSide ? { viewColumn: vscode.ViewColumn.Beside, preview: false } : {}
376 | )
377 | }
378 |
379 | private loadFiles = async (webviewPanel: vscode.WebviewPanel) => {
380 | const workspaceRootUri = vscode.workspace.workspaceFolders?.[0].uri
381 | if (!workspaceRootUri) return
382 |
383 | const files = await fg(
384 | [
385 | workspaceRootUri.path + '/**/*',
386 | `!${workspaceRootUri.path}/.git`,
387 | `!${workspaceRootUri.path}/.vscode`,
388 | `!${workspaceRootUri.path}/**/node_modules`,
389 | ],
390 | { dot: true }
391 | )
392 | const parsedFiles = files.map(
393 | file => `.${file.split(workspaceRootUri.path)[1]}`
394 | )
395 |
396 | await webviewPanel.webview.postMessage({
397 | command: 'updateFiles',
398 | files: parsedFiles,
399 | })
400 | }
401 |
402 | private loadFileContents = async (
403 | webviewPanel: vscode.WebviewPanel,
404 | filePath: string
405 | ) => {
406 | const workspaceRootUri = vscode.workspace.workspaceFolders?.[0].uri
407 | if (!workspaceRootUri) return
408 | if (!filePath) return
409 |
410 | const fileUri = vscode.Uri.joinPath(workspaceRootUri, filePath)
411 | const document = await vscode.workspace.openTextDocument(fileUri)
412 | const rawText = document.getText()
413 |
414 | await webviewPanel.webview.postMessage({
415 | command: 'returnFileContents',
416 | file: filePath,
417 | contents: rawText,
418 | })
419 | }
420 |
421 | private getRepoDetails(): Promise<{ name?: string; owner?: string }> {
422 | return new Promise(async (resolve, reject) => {
423 | try {
424 | const gitClient = new VSCodeGit()
425 | await gitClient.waitForRepo()
426 | const { name, owner } = gitClient.repoDetails
427 | resolve({ name, owner })
428 | } catch (e) {
429 | reject('Couldnt activate git')
430 | }
431 | })
432 | }
433 |
434 | private loadUrlContents = async (
435 | webviewPanel: vscode.WebviewPanel,
436 | url: string
437 | ) => {
438 | // FIX: For whatever reason, we're getting an undefined URL when the extension mounts with a certain Flat YML
439 | if (!url) return
440 | const res = await fetch(url)
441 | const contents = await res.text()
442 |
443 | await webviewPanel.webview.postMessage({
444 | command: 'returnUrlContents',
445 | url: url,
446 | contents: contents,
447 | })
448 | }
449 | }
450 |
451 | interface ShowEditorOptions {
452 | isPreview?: boolean
453 | onSide?: boolean
454 | }
455 | interface SecretData {
456 | fieldName: string
457 | value: string
458 | }
459 |
--------------------------------------------------------------------------------
/src/git.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import { API, GitExtension } from '../git'
3 |
4 | const GitUrlParse = require('git-url-parse')
5 |
6 | export class VSCodeGit {
7 | extension: GitExtension
8 | api: API
9 |
10 | constructor() {
11 | const gitExtension = vscode.extensions.getExtension('vscode.git')?.exports
12 |
13 | if (!gitExtension) {
14 | throw new Error('Git extension not found')
15 | }
16 |
17 | this.extension = gitExtension
18 | this.api = gitExtension.getAPI(1)
19 | }
20 |
21 | waitForRepo(): Promise<{ name: string; owner: string }> {
22 | let count = 0
23 |
24 | return new Promise((resolve, reject) => {
25 | let interval = setInterval(() => {
26 | try {
27 | const remotes = this.repository.state.remotes
28 | if (remotes.length > 0) {
29 | const remote = remotes[0]
30 | const parsed = GitUrlParse(remote.pushUrl)
31 | resolve({ name: parsed.name, owner: parsed.owner })
32 | } else {
33 | if (count === 3) {
34 | clearInterval(interval)
35 | reject(new Error("Couldn't get repo details"))
36 | }
37 | count++
38 | }
39 | } catch (e) {
40 | reject(new Error("Couldn't get repo details"))
41 | }
42 | }, 1000)
43 | })
44 | }
45 |
46 | get repoDetails() {
47 | const remotes = this.repository.state.remotes
48 | if (remotes.length === 0) {
49 | throw new Error(
50 | "No remotes found. Are you sure you've created an upstream repo?"
51 | )
52 | }
53 |
54 | const remote = remotes[0]
55 | const parsed = GitUrlParse(remote.pushUrl)
56 | return {
57 | name: parsed.name,
58 | owner: parsed.owner,
59 | }
60 | }
61 |
62 | get repository() {
63 | return this.api.repositories[0]
64 | }
65 |
66 | get workingTreeChanges() {
67 | if (!this.repository) {
68 | throw new Error("No repository found. Are you sure you're in a repo?")
69 | }
70 |
71 | return this.repository.state.workingTreeChanges
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/lib.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 |
3 | export function getNonce() {
4 | let text = ''
5 | const possible =
6 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
7 | for (let i = 0; i < 32; i++) {
8 | text += possible.charAt(Math.floor(Math.random() * possible.length))
9 | }
10 | return text
11 | }
12 |
13 | interface GetSessionParams {
14 | createIfNone: boolean
15 | }
16 |
17 | const GITHUB_AUTH_PROVIDER_ID = 'github'
18 | const SCOPES = ['user:email', 'repo']
19 |
20 | export function getSession(params: GetSessionParams) {
21 | const { createIfNone } = params
22 | return vscode.authentication.getSession(GITHUB_AUTH_PROVIDER_ID, SCOPES, {
23 | createIfNone,
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import * as vscode from 'vscode'
3 |
4 | export type PullBaseConfig = {
5 | downloaded_filename?: string
6 | postprocess?: string
7 | }
8 | export type PullHttpConfig = {
9 | http_url: string
10 | } & PullBaseConfig
11 |
12 | export type PullSqlConfig = {
13 | sql_connstring: string
14 | sql_queryfile: string
15 | } & PullBaseConfig
16 |
17 | export type PullConfig = PullHttpConfig | PullSqlConfig
18 |
19 | export type PushConfig = {} // TODO: once we have a push action
20 |
21 | export type PullStep = {
22 | name: string
23 | uses: 'githubocto/flat-pull@v1'
24 | with: PullConfig
25 | }
26 |
27 | export type PushStep = {
28 | name: string
29 | uses: 'githubocto/flat-push@v1'
30 | with: PushConfig
31 | }
32 |
33 | export type CheckoutStep = {
34 | name: string
35 | uses: 'actions/checkout@v2'
36 | }
37 |
38 | export type FlatStep = {
39 | name: 'Fetch data'
40 | uses: 'githubocto/flat@main'
41 | with: PullConfig
42 | }
43 |
44 | export type DenoStep = {
45 | name: 'Setup deno'
46 | uses: 'denoland/setup-deno@main'
47 | with: {
48 | 'deno-version': 'v1.10.x'
49 | }
50 | }
51 |
52 | export type Step = CheckoutStep | FlatStep | DenoStep
53 |
54 | export type FlatJob = {
55 | 'runs-on': string
56 | steps: Step[]
57 | }
58 |
59 | interface OnFlatState {
60 | workflow_dispatch?: any
61 | push?: {
62 | paths: string[]
63 | }
64 | schedule: {
65 | cron: string
66 | }[]
67 | }
68 | export type FlatState = {
69 | name: string
70 | on: OnFlatState
71 | jobs: {
72 | scheduled: {
73 | steps: Step[]
74 | }
75 | }
76 | }
77 |
78 | export type FlatYamlStep = {
79 | name: string
80 | uses: string
81 | with?: {
82 | [k: string]: string
83 | }
84 | }
85 |
86 | export type FlatYamlJob = {
87 | 'runs-on': 'ubuntu-latest'
88 | steps: [
89 | {
90 | name: 'Setup deno'
91 | uses: 'denoland/setup-deno@main'
92 | with: {
93 | 'deno-version': 'v1.10.x'
94 | }
95 | },
96 | {
97 | name: 'Checkout repo'
98 | uses: 'actions/checkout@v2'
99 | },
100 | ...Array
101 | ]
102 | }
103 |
104 | export type FlatYamlDoc = {
105 | name: 'Flat'
106 | on: {
107 | push?: {
108 | paths: ['.github/workflows/flat.yml']
109 | }
110 | workflow_dispatch: null
111 | schedule?: [
112 | {
113 | cron: string
114 | }
115 | ]
116 | }
117 | jobs: {
118 | scheduled: FlatYamlJob
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/webviews/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import flatten from 'lodash-es/flatten'
3 | import uniq from 'lodash-es/uniq'
4 | import Jobs from './Jobs'
5 | import useFlatConfigStore from './store'
6 | import Triggers from './Triggers'
7 | import { flatStateValidationSchema } from './validation'
8 | import { VSCodeAPI } from './VSCodeAPI'
9 | import { FlatStep, PullSqlConfig } from '../../types'
10 | import { ErrorState } from './error-state'
11 | import {
12 | VSCodeButton,
13 | VSCodeDivider,
14 | VSCodeLink,
15 | } from '@vscode/webview-ui-toolkit/react'
16 |
17 | interface AppProps {}
18 |
19 | function App({}: AppProps) {
20 | const { state, setErrors, isStubData, gitRepo } = useFlatConfigStore()
21 |
22 | if (!gitRepo) {
23 | return
24 | }
25 |
26 | const showErrorState = state.jobs.scheduled.steps
27 | .filter(step => step.uses.includes('githubocto/flat'))
28 | .some(step => {
29 | return !Boolean((step as FlatStep)?.with?.downloaded_filename)
30 | })
31 |
32 | useEffect(() => {
33 | flatStateValidationSchema
34 | .validate(state, { abortEarly: false })
35 | .then(function () {
36 | setErrors([])
37 | })
38 | .catch(function (err) {
39 | setErrors(err.inner)
40 | })
41 |
42 | if (isStubData) return
43 |
44 | // Add push paths for all postprocessing files to "state"
45 | let cloned = { ...state }
46 |
47 | if (cloned.on.push) {
48 | // @ts-ignore
49 | cloned.on.push.paths = uniq([
50 | '.github/workflows/flat.yml',
51 | ...flatten(
52 | state.jobs.scheduled.steps.map(step => {
53 | let files = []
54 | if (!(step as FlatStep).with) return []
55 | if ((step as FlatStep).with.postprocess !== undefined) {
56 | files.push((step as FlatStep).with.postprocess)
57 | }
58 | if (((step as FlatStep).with as PullSqlConfig).sql_queryfile) {
59 | files.push(
60 | ((step as FlatStep).with as PullSqlConfig).sql_queryfile
61 | )
62 | }
63 | return files
64 | })
65 | ),
66 | ])
67 | }
68 |
69 | VSCodeAPI.postMessage({
70 | type: 'updateText',
71 | data: cloned,
72 | })
73 | }, [state])
74 |
75 | const handleOpenRaw = () => {
76 | VSCodeAPI.postMessage({
77 | type: 'openEditor',
78 | data: { isPreview: false, onSide: false },
79 | })
80 | }
81 |
82 | const actionsUrl = gitRepo && `https://github.com/${gitRepo}/actions`
83 |
84 | return (
85 | ]
86 |
100 |
101 |
102 |
103 |
104 |
105 | {showErrorState ? (
106 |
107 |
108 |
109 | Make sure all of your steps have a{' '}
110 | downloaded_filename specified!
111 |
112 |
113 | ) : (
114 |
115 | Commit, push, and check out your new Action on{' '}
116 |
117 | on GitHub
118 | {' '}
119 | It should run automatically, once pushed.
120 |
121 | )}
122 |
123 |
124 | )
125 | }
126 |
127 | export default App
128 |
--------------------------------------------------------------------------------
/src/webviews/src/Header.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react'
2 |
3 | type HeaderProps = {
4 | title: string
5 | description: string
6 | hasHoverState?: boolean
7 | }
8 |
9 | const Header: FunctionComponent = props => {
10 | const hasHoverState = props.hasHoverState === false ? false : true
11 | return (
12 |
17 |
18 | {props.title}
19 |
20 | {props.description}
21 | {props.children && {props.children}
}
22 |
23 | )
24 | }
25 |
26 | export default Header
27 |
--------------------------------------------------------------------------------
/src/webviews/src/Jobs.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react'
2 |
3 | import Header from './Header'
4 | import { Step } from './Step'
5 | import useFlatConfigStore from './store'
6 | import type { FlatStep } from './../../types'
7 | import { VSCodeButton, VSCodeDivider } from '@vscode/webview-ui-toolkit/react'
8 |
9 | interface JobsProps {}
10 |
11 | const STEP_STUBS = {
12 | http: {
13 | name: 'Fetch data',
14 | uses: 'githubocto/flat@v3',
15 | with: {
16 | http_url: '',
17 | },
18 | },
19 | sql: {
20 | name: 'Fetch data',
21 | uses: 'githubocto/flat@v3',
22 | with: {
23 | sql_connstring: '',
24 | sql_queryfile: '',
25 | },
26 | },
27 | }
28 |
29 | const Jobs: FunctionComponent = props => {
30 | const { state, update } = useFlatConfigStore()
31 |
32 | const handleJobAdded = (type: 'http' | 'sql') => {
33 | update(store => {
34 | // @ts-ignore
35 | store.state.jobs.scheduled.steps.push(STEP_STUBS[type])
36 | })
37 | }
38 |
39 | const steps = state.jobs.scheduled.steps
40 | .slice(2)
41 | .map((j, i) => )
42 |
43 | return (
44 |
45 |
46 |
47 |
48 | Where to get data from
49 |
50 |
51 |
52 | Flat can fetch data from HTTP endpoints or SQL queries.
53 |
54 |
55 |
58 |
78 |
79 | )
80 | }
81 |
82 | export default Jobs
83 |
--------------------------------------------------------------------------------
/src/webviews/src/Setting.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react'
2 |
3 | type SettingProps = {
4 | title: string
5 | }
6 |
7 | const Setting: FunctionComponent = props => {
8 | return (
9 |
10 |
{props.title}
{' '}
11 |
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default Setting
19 |
--------------------------------------------------------------------------------
/src/webviews/src/Step.tsx:
--------------------------------------------------------------------------------
1 | import { VSCodeButton, VSCodeTag } from '@vscode/webview-ui-toolkit/react'
2 | import React, { FunctionComponent } from 'react'
3 | import type { FlatStep } from '../../types'
4 |
5 | import { FilePicker } from './settings/FilePicker'
6 | import { StepConfig } from './StepConfig'
7 | import useFlatConfigStore from './store'
8 |
9 | type StepProps = {
10 | step: FlatStep
11 | index: number
12 | }
13 |
14 | export const Step: FunctionComponent = props => {
15 | const update = useFlatConfigStore(state => state.update)
16 |
17 | const handlePostprocessChange = (newPath?: string) => {
18 | update(store => {
19 | ;(
20 | store.state.jobs.scheduled.steps[props.index] as FlatStep
21 | ).with.postprocess = newPath
22 | })
23 | }
24 |
25 | const handleRemoveStep = () => {
26 | update(store => {
27 | store.state.jobs.scheduled.steps.splice(props.index, 1)
28 | })
29 | }
30 |
31 | return (
32 |
33 |
34 |
35 | #{props.index + 1 - 2} Fetch data via{' '}
36 | {'with' in props.step && 'sql_queryfile' in props.step.with
37 | ? 'SQL'
38 | : 'HTTP'}
39 |
40 |
41 |
42 | Remove
43 |
44 |
45 |
46 |
47 | {
53 | handlePostprocessChange(newPath || undefined)
54 | }}
55 | isClearable
56 | />
57 |
58 |
59 | )
60 | }
61 |
62 | export default Step
63 |
--------------------------------------------------------------------------------
/src/webviews/src/StepConfig.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import type { Step, FlatStep, PullHttpConfig, PullSqlConfig } from '../../types'
3 | import { FilePicker } from './settings/FilePicker'
4 | import { HttpEndpointPreview } from './settings/HttpEndpointPreview'
5 | import SecretInput from './settings/SecretInput'
6 | import useFlatConfigStore from './store'
7 | import { VSCodeTextField } from '@vscode/webview-ui-toolkit/react'
8 |
9 | interface StepConfigProps {
10 | step: Step
11 | stepIndex: number
12 | }
13 |
14 | export function StepConfig(props: StepConfigProps) {
15 | const { update, workspace } = useFlatConfigStore()
16 |
17 | const handleHttpValueChange = (stepName: string, newValue?: string) => {
18 | update(store => {
19 | const step = store.state.jobs.scheduled.steps[props.stepIndex] as FlatStep
20 | // @ts-ignore
21 | ;(step.with as PullHttpConfig)[stepName] = newValue
22 | })
23 | }
24 | const handleSqlValueChange = (stepName: string, newValue: string) => {
25 | update(store => {
26 | const step = store.state.jobs.scheduled.steps[props.stepIndex] as FlatStep
27 | // @ts-ignore
28 | ;(step.with as PullSqlConfig)[stepName] = newValue
29 | })
30 | }
31 |
32 | if ('with' in props.step && 'http_url' in props.step.with) {
33 | return (
34 | <>
35 |
36 |
{
44 | handleHttpValueChange(
45 | 'downloaded_filename',
46 | e.target.value || undefined
47 | )
48 | }
49 | }
50 | >
51 | Downloaded Filename (required)
52 |
53 |
54 | The filename where you want the results to be saved. This file
55 | doesn't need to exist yet.
56 |
57 |
58 |
59 |
{
65 | handleHttpValueChange('http_url', e.target.value)
66 | }
67 | }
68 | value={props.step.with.http_url}
69 | >
70 | Endpoint url (required)
71 |
72 |
73 | Which endpoint should we pull data from? This needs to be a stable,
74 | unchanging URL.
75 |
76 |
77 |
78 |
79 |
80 | >
81 | )
82 | } else if ('with' in props.step && 'sql_queryfile' in props.step.with) {
83 | return (
84 | <>
85 |
86 |
handleSqlValueChange('downloaded_filename', e.target.value)
94 | }
95 | >
96 | Downloaded filename (required)
97 |
98 |
99 | The filename (with a csv or json extension) where you want the
100 | results to be saved. This file doesn't need to exist yet.
101 |
102 |
103 | {
109 | handleSqlValueChange('sql_queryfile', newPath)
110 | }}
111 | />
112 |
117 | handleSqlValueChange('sql_connstring', newValue)
118 | }
119 | />
120 | >
121 | )
122 | } else {
123 | return null
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/webviews/src/Triggers.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import CronChooser from './settings/CronChooser'
3 | import useFlatConfigStore from './store'
4 |
5 | type TriggersProps = {}
6 | export function Triggers(props: TriggersProps) {
7 | const { state, update } = useFlatConfigStore()
8 |
9 | const cron = state.on?.schedule?.[0]?.cron || ''
10 |
11 | const handleScheduleChange = (schedule: string) => {
12 | update(store => {
13 | // not likely, but handling borked state
14 | if (Array.isArray(store.state.on)) {
15 | store.state.on = { schedule: [{ cron: '' }] }
16 | }
17 | if (!store.state.on?.schedule || !store.state.on?.schedule.length) {
18 | store.state.on.schedule = [{ cron: '' }]
19 | }
20 | store.state.on.schedule[0].cron = schedule
21 | })
22 | }
23 |
24 | return (
25 |
26 |
27 |
28 | When to update the data
29 |
30 |
31 |
32 |
33 | )
34 | }
35 |
36 | export default Triggers
37 |
--------------------------------------------------------------------------------
/src/webviews/src/VSCodeAPI.tsx:
--------------------------------------------------------------------------------
1 | declare const acquireVsCodeApi: Function
2 |
3 | interface VSCodeApi {
4 | getState: () => any
5 | setState: (newState: any) => any
6 | postMessage: (message: any) => void
7 | }
8 |
9 | // declare global {
10 | // interface Window {
11 | // acquireVsCodeApi(): VSCodeApi
12 | // }
13 | // }
14 |
15 | class VSCodeWrapper {
16 | private readonly vscodeApi: VSCodeApi = acquireVsCodeApi()
17 |
18 | /**
19 | * Send message to the extension framework.
20 | * @param message
21 | */
22 | public postMessage(message: any): void {
23 | this.vscodeApi.postMessage(message)
24 | }
25 |
26 | /**
27 | * Add listener for messages from extension framework.
28 | * @param callback called when the extension sends a message
29 | * @returns function to clean up the message eventListener.
30 | */
31 | public onMessage(callback: (message: any) => void): () => void {
32 | window.addEventListener('message', callback)
33 | return () => window.removeEventListener('message', callback)
34 | }
35 | }
36 |
37 | // Singleton to prevent multiple fetches of VsCodeAPI.
38 | export const VSCodeAPI: VSCodeWrapper = new VSCodeWrapper()
39 |
--------------------------------------------------------------------------------
/src/webviews/src/Workflow.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react'
2 |
3 | type WorkflowProps = {}
4 |
5 | const Workflow: FunctionComponent = props => {
6 | return
7 | }
8 |
9 | export default Workflow
10 |
--------------------------------------------------------------------------------
/src/webviews/src/error-state.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'
3 |
4 | import { VSCodeAPI } from './VSCodeAPI'
5 |
6 | export function ErrorState() {
7 | const [isRefreshing, setIsRefreshing] = useState(false)
8 |
9 | const handleRetry = () => {
10 | setIsRefreshing(true)
11 | VSCodeAPI.postMessage({
12 | type: 'refreshGitDetails',
13 | })
14 | }
15 |
16 | return (
17 |
18 |
19 | Please ensure you're working out of a Git repository with a valid
20 | upstream remote set.
21 |
22 |
23 |
24 | {isRefreshing ? 'Refreshing...' : 'Refresh'}
25 |
26 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/src/webviews/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 | import { useFlatConfigStore } from './store'
5 | import './vscode.css'
6 | import { VSCodeAPI } from './VSCodeAPI'
7 |
8 | // TODO: Type the incoming config data
9 | let config: any = {}
10 | let workspace = ''
11 | let gitRepo = ''
12 |
13 | const root = document.getElementById('root')
14 |
15 | function transformConfig(config: any) {
16 | if (!config)
17 | config = {
18 | on: {
19 | workflow_dispatch: {},
20 | push: {
21 | paths: ['.github/workflows/flat.yml'],
22 | },
23 | },
24 | }
25 | if (!config.on) config.on = {}
26 | if (!config.on.push) config.on.push = {}
27 | if (!config.on.push.paths) config.on.push.paths = []
28 | if (!config.jobs) config.jobs = {}
29 | if (!config.jobs.scheduled)
30 | config.jobs.scheduled = {
31 | 'runs-on': 'ubuntu-latest',
32 | steps: [
33 | {
34 | name: 'Setup deno',
35 | uses: 'denoland/setup-deno@main',
36 | with: {
37 | 'deno-version': 'v1.10.x',
38 | },
39 | },
40 | {
41 | name: 'Check out repo',
42 | uses: 'actions/checkout@v2',
43 | },
44 | ],
45 | }
46 |
47 | return config
48 | }
49 |
50 | config = transformConfig(config)
51 |
52 | if (root) {
53 | workspace = root.getAttribute('data-workspace') || ''
54 | gitRepo = root.getAttribute('data-gitrepo') || ''
55 | }
56 |
57 | useFlatConfigStore.setState({
58 | // @ts-ignore
59 | state: config,
60 | workspace,
61 | gitRepo,
62 | isStubData: true,
63 | })
64 |
65 | VSCodeAPI.postMessage({
66 | type: 'refreshFiles',
67 | })
68 |
69 | VSCodeAPI.postMessage({
70 | type: 'refreshState',
71 | })
72 |
73 | window.addEventListener('message', e => {
74 | // @ts-ignore
75 | const message = e.data
76 | if (message.command === 'updateState') {
77 | useFlatConfigStore.setState({
78 | state: transformConfig(message.config),
79 | isStubData: false,
80 | })
81 | } else if (message.command === 'updateFiles') {
82 | useFlatConfigStore.setState({
83 | files: message.files,
84 | })
85 | }
86 | })
87 |
88 | ReactDOM.render(
89 |
90 |
91 | ,
92 | document.getElementById('root')
93 | )
94 |
--------------------------------------------------------------------------------
/src/webviews/src/settings/CronChooser.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent, useEffect } from 'react'
2 | import { ErrorBoundary } from 'react-error-boundary'
3 | import {
4 | VSCodeRadioGroup,
5 | VSCodeRadio,
6 | VSCodeTextField,
7 | VSCodeLink,
8 | } from '@vscode/webview-ui-toolkit/react'
9 | import { RadioGroupOrientation } from '@vscode/webview-ui-toolkit'
10 | import * as cronstrue from 'cronstrue'
11 | import { useState } from 'react'
12 |
13 | type CronChooserProps = {
14 | value: string
15 | onChange: (schedule: string) => void
16 | }
17 |
18 | const defaultSchedules = {
19 | fiveMinutes: '*/5 * * * *',
20 | hour: '0 * * * *',
21 | day: '0 0 * * *',
22 | }
23 |
24 | const CronFallback = ({ error }: { error: any }) => {
25 | return (
26 |
27 |
28 | {error}
29 |
30 | )
31 | }
32 |
33 | const ValidateCron = ({ value }: { value: string }) => {
34 | const feedback = cronstrue.toString(value)
35 | return (
36 |
37 |
38 |
39 | Will run:{' '}
40 | {feedback === 'Every minute' ? 'Every five minutes' : feedback}
41 |
42 |
43 | )
44 | }
45 |
46 | const determineInitialCustomValue = (schedule?: string) => {
47 | if (!schedule) return false
48 | return !Object.values(defaultSchedules).includes(schedule)
49 | }
50 |
51 | const CustomCronTextField = ({
52 | value,
53 | onChange,
54 | }: {
55 | value: string
56 | onChange: (val: string) => void
57 | }) => {
58 | const [localValue, setValue] = useState(value)
59 |
60 | return (
61 |
62 |
{
67 | setValue(e.target.value)
68 | onChange(e.target.value)
69 | }
70 | }
71 | placeholder="Enter custom schedule"
72 | >
73 | Enter a custom CRON schedule
74 |
75 |
76 |
77 |
78 |
79 |
80 | Need help with CRON syntax?
81 |
82 |
83 |
84 | )
85 | }
86 |
87 | const CronChooser: FunctionComponent = props => {
88 | const [showCustom, setShowCustom] = React.useState(() => {
89 | return determineInitialCustomValue(props.value)
90 | })
91 |
92 | const handleRadioChange = (value: string) => {
93 | const custom = value === 'custom'
94 | setShowCustom(custom)
95 | if (!custom) {
96 | props.onChange(value)
97 | }
98 | }
99 |
100 | useEffect(() => {
101 | if (!props.value) return
102 | const isCustom = determineInitialCustomValue(props.value)
103 | if (isCustom && !showCustom) {
104 | setShowCustom(true)
105 | }
106 | }, [props.value])
107 |
108 | return (
109 |
110 |
111 |
112 | handleRadioChange(e.target.value)
116 | }
117 | checked={!showCustom && props.value === defaultSchedules.fiveMinutes}
118 | value={defaultSchedules.fiveMinutes}
119 | >
120 | Every five minutes
121 |
122 | handleRadioChange(e.target.value)
126 | }
127 | checked={!showCustom && props.value === defaultSchedules.hour}
128 | value={defaultSchedules.hour}
129 | >
130 | Every hour
131 |
132 | handleRadioChange(e.target.value)
137 | }
138 | checked={!showCustom && props.value === defaultSchedules.day}
139 | value={defaultSchedules.day}
140 | >
141 | Every day
142 |
143 | handleRadioChange(e.target.value)
147 | }
148 | checked={showCustom}
149 | value="custom"
150 | >
151 | Custom
152 |
153 |
154 | {showCustom && (
155 |
156 |
157 |
158 | )}
159 |
160 | )
161 | }
162 |
163 | export default CronChooser
164 |
--------------------------------------------------------------------------------
/src/webviews/src/settings/FieldWithDescription.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react'
2 |
3 | type FieldWithDescriptionProps = {
4 | title: string
5 | }
6 |
7 | const FieldWithDescription: FunctionComponent = props => {
8 | return (
9 |
10 |
11 | {props.title}
12 |
13 | {props.children}
14 |
15 | )
16 | }
17 |
18 | export default FieldWithDescription
19 |
--------------------------------------------------------------------------------
/src/webviews/src/settings/FilePicker.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import React from 'react'
3 |
4 | import {
5 | Combobox,
6 | ComboboxInput,
7 | ComboboxPopover,
8 | ComboboxList,
9 | ComboboxOption,
10 | } from '@reach/combobox'
11 | import '@reach/combobox/styles.css'
12 |
13 | import { FilePreview } from './FilePreview'
14 | import { VSCodeAPI } from '../VSCodeAPI'
15 | import useFlatConfigStore from '../store'
16 | import {
17 | VSCodeButton,
18 | VSCodeOption,
19 | VSCodeTextField,
20 | } from '@vscode/webview-ui-toolkit/react'
21 | import { useEffect } from 'react'
22 |
23 | interface FilePickerProps {
24 | value?: string
25 | onChange: (newValue: string) => void
26 | title: string
27 | label: string
28 | accept?: string
29 | isClearable?: boolean
30 | }
31 |
32 | export function FilePicker(props: FilePickerProps) {
33 | const { label, value, onChange, accept = '', title } = props
34 | const [localValue, setLocalValue] = React.useState(value)
35 | const files = useFlatConfigStore(state => state.files || [])
36 |
37 | useEffect(() => {
38 | if (localValue === '' && Boolean(value)) {
39 | onChange(null)
40 | }
41 | }, [value, localValue])
42 |
43 | const acceptedExtensions = accept.split(',')
44 |
45 | const filteredFiles = files
46 | .filter(file => {
47 | return (
48 | !acceptedExtensions.length ||
49 | acceptedExtensions.includes(`.${file.split('.').slice(-1)}`)
50 | )
51 | })
52 | .filter(file => {
53 | if (!localValue) return true
54 | return file.includes(localValue)
55 | })
56 |
57 | const handleFocus = () => {
58 | VSCodeAPI.postMessage({
59 | type: 'refreshFiles',
60 | })
61 | }
62 |
63 | const handlePreview = (path: string) => {
64 | VSCodeAPI.postMessage({
65 | type: 'previewFile',
66 | data: path,
67 | })
68 | }
69 |
70 | return (
71 |
72 |
{
76 | setLocalValue(value)
77 | onChange(value)
78 | }}
79 | >
80 | {
85 | setLocalValue(e.target.value)
86 | }}
87 | className="w-full"
88 | as={VSCodeTextField}
89 | placeholder={files.length === 0 ? 'Loading files...' : 'Pick a file'}
90 | >
91 | {title}
92 |
93 |
94 |
95 | {!files && Loading...
}
96 | {filteredFiles.length > 0 && (
97 |
98 | {filteredFiles.map(file => {
99 | return (
100 |
106 | )
107 | })}
108 |
109 | )}
110 | {!filteredFiles.length && (
111 |
112 | No files found
113 | {accept && ` with the extensions ${accept}`}
114 | {localValue && ` that include "${localValue}"`}
115 |
116 | )}
117 |
118 |
119 |
120 |
121 | {label}
122 |
123 |
124 | {files.includes(localValue) && (
125 |
126 |
127 |
128 | {
131 | handlePreview(localValue)
132 | }}
133 | >
134 |
135 | View file
136 |
137 |
138 |
139 | )}
140 |
141 | )
142 | }
143 |
--------------------------------------------------------------------------------
/src/webviews/src/settings/FilePreview.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react'
2 | import { VSCodeAPI } from '../VSCodeAPI'
3 |
4 | type FilePreviewProps = {
5 | file: string
6 | }
7 |
8 | export const FilePreview: FunctionComponent = props => {
9 | const { file } = props
10 | const fileRef = React.useRef('')
11 | const [fileContent, setFileContent] = React.useState(undefined)
12 |
13 | React.useEffect(() => {
14 | setFileContent(undefined)
15 | fileRef.current = file
16 | VSCodeAPI.postMessage({
17 | type: 'getFileContents',
18 | data: file,
19 | })
20 | }, [file])
21 |
22 | React.useEffect(() => {
23 | window.addEventListener('message', e => {
24 | const message = e.data
25 | if (message.command !== 'returnFileContents') return
26 | if (message.file !== fileRef.current) return
27 | setFileContent(message.contents)
28 | })
29 | }, [])
30 |
31 | if (!fileContent) return null
32 |
33 | return (
34 |
35 | {(fileContent || '').slice(0, 100000)}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/webviews/src/settings/HttpEndpointPreview.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react'
2 | import { VSCodeAPI } from '../VSCodeAPI'
3 | import { debounce } from 'ts-debounce'
4 |
5 | type HttpEndpointPreviewProps = {
6 | url: string
7 | }
8 |
9 | export const HttpEndpointPreview: FunctionComponent = props => {
10 | const { url } = props
11 | const urlRef = React.useRef('')
12 | const [fileContent, setFileContent] = React.useState(undefined)
13 |
14 | const fetchData = async () => {
15 | VSCodeAPI.postMessage({
16 | type: 'getUrlContents',
17 | data: urlRef.current,
18 | })
19 | }
20 | const debounceFetchData = React.useCallback(debounce(fetchData, 600), [])
21 |
22 | React.useEffect(() => {
23 | urlRef.current = url
24 | setFileContent(undefined)
25 | debounceFetchData()
26 | }, [url])
27 |
28 | React.useEffect(() => {
29 | window.addEventListener('message', e => {
30 | const message = e.data
31 | if (message.command !== 'returnUrlContents') return
32 | if (message.url !== urlRef.current) return
33 | setFileContent(message.contents)
34 | })
35 | }, [])
36 |
37 | if (!fileContent) return null
38 |
39 | return (
40 |
41 | {(fileContent || '').slice(0, 100000)}
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/src/webviews/src/settings/SecretInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react'
2 | import { VSCodeAPI } from './../VSCodeAPI'
3 | import { VSCodeButton, VSCodeTextField } from '@vscode/webview-ui-toolkit/react'
4 | import { customAlphabet } from 'nanoid'
5 | const nanoid = customAlphabet('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', 6)
6 |
7 | type SecretInputProps = {
8 | title: string
9 | label: string
10 | value: string
11 | handleChange: (newValue: string) => void
12 | }
13 |
14 | const SecretInput: FunctionComponent = props => {
15 | const { title, label, value, handleChange } = props
16 |
17 | const [localValue, setLocalValue] = React.useState(value)
18 | const [isDirty, setIsDirty] = React.useState(false)
19 | const [didError, setDidError] = React.useState(false)
20 | const [doesExist, setDoesExist] = React.useState(
21 | value &&
22 | (value.split(' ')[1] || '').split('_')[0] === 'secrets.FLAT' &&
23 | (value.split(' ')[1] || '').split('_')[2] === 'CONNSTRING'
24 | )
25 |
26 | const fieldName = React.useMemo(
27 | () => (doesExist ? value : `\${{ secrets.FLAT_${nanoid()}_CONNSTRING }}`),
28 | []
29 | )
30 | const innerFieldName = fieldName.split(' ')[1].replace('secrets.', '')
31 | const isSavedAsSecret =
32 | fieldName.split('_')[0] === '${{ secrets.FLAT' &&
33 | fieldName.split('_')[2] === 'CONNSTRING }}' &&
34 | localValue === fieldName
35 |
36 | const handleSave = async () => {
37 | VSCodeAPI.postMessage({
38 | type: 'storeSecret',
39 | data: { fieldName: innerFieldName, value: localValue },
40 | })
41 | }
42 |
43 | React.useEffect(() => {
44 | window.addEventListener('message', e => {
45 | // @ts-ignore
46 | const message = e.data
47 | if (message.command !== 'storeSecretResponse') return
48 | if (message.fieldName !== innerFieldName) return
49 |
50 | if (message.status === 'success') {
51 | handleChange(fieldName)
52 | setIsDirty(false)
53 | setDoesExist(true)
54 | setLocalValue(fieldName)
55 | } else {
56 | setDidError(true)
57 | }
58 | })
59 | }, [])
60 | return (
61 |
62 |
104 |
105 | {label}
106 |
107 | {didError && (
108 |
109 | Something went wrong, please try again.
110 |
111 | )}
112 |
113 | )
114 | }
115 |
116 | export default SecretInput
117 |
--------------------------------------------------------------------------------
/src/webviews/src/store.ts:
--------------------------------------------------------------------------------
1 | import produce, { Draft } from 'immer'
2 | import create, { State, StateCreator } from 'zustand'
3 | import type { FlatState } from '../../types'
4 |
5 | const immer = (
6 | config: StateCreator) => void) => void>
7 | ): StateCreator => (set, get, api) =>
8 | config(fn => set(produce(fn)), get, api)
9 |
10 | interface ValidationError {
11 | path: string
12 | errors: string[]
13 | message: string
14 | type: string
15 | }
16 |
17 | type FlatStoreState = {
18 | state: FlatState
19 | workspace: string
20 | gitRepo: string
21 | errors: ValidationError[]
22 | update: (fn: (draft: Draft) => void) => void
23 | setErrors: (errors: ValidationError[]) => void
24 | files?: string[]
25 | isStubData: boolean
26 | }
27 |
28 | export const useFlatConfigStore = create(
29 | immer(set => ({
30 | errors: [],
31 | workspace: '',
32 | gitRepo: '',
33 | isStubData: true,
34 | state: {
35 | name: 'Flat',
36 | on: {
37 | workflow_dispatch: undefined,
38 | push: {
39 | paths: ['.github/workflows/flat.yml'],
40 | },
41 | schedule: [
42 | {
43 | cron: '0 0 * * *',
44 | },
45 | ],
46 | },
47 | jobs: {
48 | scheduled: {
49 | steps: [
50 | {
51 | name: 'Setup deno',
52 | uses: 'denoland/setup-deno@main',
53 | with: {
54 | 'deno-version': 'v1.10.x',
55 | },
56 | },
57 | {
58 | name: 'Check out repo',
59 | uses: 'actions/checkout@v2',
60 | },
61 | ],
62 | },
63 | },
64 | },
65 | update: fn => {
66 | set(fn)
67 | },
68 | setErrors: errors => {
69 | set(draft => {
70 | draft.errors = errors
71 | })
72 | },
73 | files: undefined,
74 | }))
75 | )
76 |
77 | export default useFlatConfigStore
78 |
--------------------------------------------------------------------------------
/src/webviews/src/validation.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import * as yup from 'yup'
3 |
4 | yup.addMethod(yup.array, 'unique', function (message) {
5 | return this.test('unique', message, function (list) {
6 | const mapper = x => x.name
7 | const set = [...new Set(list.map(mapper))]
8 | const isUnique = list.length === set.length
9 | if (isUnique) {
10 | return true
11 | }
12 | const idx = list.findIndex((l, i) => mapper(l) !== set[i])
13 | return this.createError({
14 | path: `jobs[${idx}].name`,
15 | message: message,
16 | })
17 | })
18 | })
19 |
20 | const jobValidationSchema = yup.object().shape({
21 | name: yup
22 | .string()
23 | .required('Please enter a job name')
24 | .matches(
25 | /^([a-zA-Z_]){1}([a-zA-Z_\-\d])*$/,
26 | "Names must start with a letter or '_' and contain only alphanumeric characters, '-', or '_'"
27 | ),
28 | })
29 |
30 | export const flatStateValidationSchema = yup.object().shape({
31 | // on: yup.shape({
32 | // push: yup.optional(
33 | // yup.shape({
34 | // branches: yup.array(),
35 | // })
36 | // ),
37 | // schedule: yup.shape({
38 | // cron: yup.string().required('Please provide a trigger schedule'),
39 | // }),
40 | // }),
41 | jobs: yup
42 | .array()
43 | .of(jobValidationSchema)
44 | // @ts-ignore
45 | .unique('Job names must be unique')
46 | .required(),
47 | })
48 |
--------------------------------------------------------------------------------
/src/webviews/src/vscode.css:
--------------------------------------------------------------------------------
1 | /* @tailwind base; */
2 |
3 | body {
4 | padding: 0;
5 | margin: 0;
6 | }
7 |
8 | @tailwind components;
9 | @tailwind utilities;
10 |
11 | [data-reach-combobox-option][aria-selected='true'] {
12 | background: var(--list-active-selection-background) !important;
13 | border: calc(var(--border-width) * 1px) solid var(--focus-border) !important;
14 | color: var(--list-active-selection-foreground) !important;
15 | }
16 |
17 | [data-reach-combobox-option]:hover {
18 | background: var(--list-active-selection-background) !important;
19 | }
20 |
21 | [data-reach-combobox-option][aria-selected='true']:hover {
22 | background: var(--list-active-selection-background) !important;
23 | }
24 |
--------------------------------------------------------------------------------
/src/webviews/types/static.d.ts:
--------------------------------------------------------------------------------
1 | /* Use this file to declare any custom file extensions for importing */
2 | /* Use this folder to also add/extend a package d.ts file, if needed. */
3 |
4 | /* CSS MODULES */
5 | declare module '*.module.css' {
6 | const classes: { [key: string]: string };
7 | export default classes;
8 | }
9 | declare module '*.module.scss' {
10 | const classes: { [key: string]: string };
11 | export default classes;
12 | }
13 | declare module '*.module.sass' {
14 | const classes: { [key: string]: string };
15 | export default classes;
16 | }
17 | declare module '*.module.less' {
18 | const classes: { [key: string]: string };
19 | export default classes;
20 | }
21 | declare module '*.module.styl' {
22 | const classes: { [key: string]: string };
23 | export default classes;
24 | }
25 |
26 | /* CSS */
27 | declare module '*.css';
28 | declare module '*.scss';
29 | declare module '*.sass';
30 | declare module '*.less';
31 | declare module '*.styl';
32 |
33 | /* IMAGES */
34 | declare module '*.svg' {
35 | const ref: string;
36 | export default ref;
37 | }
38 | declare module '*.bmp' {
39 | const ref: string;
40 | export default ref;
41 | }
42 | declare module '*.gif' {
43 | const ref: string;
44 | export default ref;
45 | }
46 | declare module '*.jpg' {
47 | const ref: string;
48 | export default ref;
49 | }
50 | declare module '*.jpeg' {
51 | const ref: string;
52 | export default ref;
53 | }
54 | declare module '*.png' {
55 | const ref: string;
56 | export default ref;
57 | }
58 |
59 | /* CUSTOM: ADD YOUR OWN HERE */
60 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import("@types/tailwindcss/tailwind-config").TailwindConfig } */
2 | module.exports = {
3 | content: ['./src/webviews/**/*.{js,jsx,ts,tsx,css}'],
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig-webview.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src/webviews"],
3 | "compilerOptions": {
4 | "module": "esnext",
5 | "target": "esnext",
6 | "moduleResolution": "node",
7 | "jsx": "preserve",
8 | "baseUrl": "./src/webviews",
9 | /* paths - import rewriting/resolving */
10 | "paths": {
11 | // If you configured any Snowpack aliases, add them here.
12 | // Add this line to get types for streaming imports (packageOptions.source="remote"):
13 | // "*": [".snowpack/types/*"]
14 | // More info: https://www.snowpack.dev/guides/streaming-imports
15 | },
16 | /* noEmit - Snowpack builds (emits) files, not tsc. */
17 | "noEmit": true,
18 | /* Additional Options */
19 | "strict": true,
20 | "skipLibCheck": true,
21 | "forceConsistentCasingInFileNames": true,
22 | "resolveJsonModule": true,
23 | "allowSyntheticDefaultImports": true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "ES2020",
5 | "lib": ["ES2020"],
6 | "outDir": "out",
7 | "sourceMap": true,
8 | "strict": true,
9 | "rootDir": "src",
10 | "skipLibCheck": true
11 | },
12 | "exclude": ["node_modules", ".vscode-test", "src/webviews"]
13 | }
14 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = {
4 | target: 'node',
5 | mode: 'development',
6 | entry: './src/extension.ts',
7 | output: {
8 | filename: 'extension.js',
9 | path: path.resolve(__dirname, 'out'),
10 | libraryTarget: 'commonjs2',
11 | devtoolModuleFilenameTemplate: '../[resource-path]',
12 | },
13 | devtool: 'source-map',
14 | externals: {
15 | vscode: 'commonjs vscode',
16 | },
17 | resolve: {
18 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
19 | extensions: ['.ts', '.js'],
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.ts$/,
25 | exclude: /node_modules/,
26 | use: [
27 | {
28 | loader: 'babel-loader',
29 | },
30 | ],
31 | },
32 | ],
33 | },
34 | }
35 |
--------------------------------------------------------------------------------