Merge branch 'main' into wasp-ai
19
.github/workflows/ci.yaml
vendored
@ -83,6 +83,11 @@ jobs:
|
|||||||
ghc --version
|
ghc --version
|
||||||
cabal --version
|
cabal --version
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
|
||||||
- name: Cache
|
- name: Cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
@ -107,7 +112,14 @@ jobs:
|
|||||||
if: matrix.os == 'ubuntu-20.04'
|
if: matrix.os == 'ubuntu-20.04'
|
||||||
run: ./run ormolu:check
|
run: ./run ormolu:check
|
||||||
|
|
||||||
- name: Compile deploy TS packages and move it into the Cabal data dir
|
- name: packages/ts-inspect - Run tests
|
||||||
|
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'macos-latest'
|
||||||
|
run: |
|
||||||
|
cd packages/ts-inspect
|
||||||
|
npm ci
|
||||||
|
npm test
|
||||||
|
|
||||||
|
- name: Compile TS packages and move it into the Cabal data dir
|
||||||
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'macos-latest'
|
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'macos-latest'
|
||||||
run: ./tools/install_packages_to_data_dir.sh
|
run: ./tools/install_packages_to_data_dir.sh
|
||||||
|
|
||||||
@ -117,11 +129,6 @@ jobs:
|
|||||||
- name: Build wasp code
|
- name: Build wasp code
|
||||||
run: cabal build all
|
run: cabal build all
|
||||||
|
|
||||||
- name: Set up Node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '18'
|
|
||||||
|
|
||||||
- name: On MacOS, skip e2e tests with Docker since it is not installed
|
- name: On MacOS, skip e2e tests with Docker since it is not installed
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
run: export WASP_E2E_TESTS_SKIP_DOCKER=1
|
run: export WASP_E2E_TESTS_SKIP_DOCKER=1
|
||||||
|
@ -94,9 +94,8 @@ This is the main repo of the Wasp universe, containing core code (mostly `waspc`
|
|||||||
|
|
||||||
Currently, Wasp is in beta, with most features flushed out and working well.
|
Currently, Wasp is in beta, with most features flushed out and working well.
|
||||||
However, there are still a lot of improvements and additions that we have in mind for the future, and we are working on them constantly, so you can expect a lot of changes and improvements in the future.
|
However, there are still a lot of improvements and additions that we have in mind for the future, and we are working on them constantly, so you can expect a lot of changes and improvements in the future.
|
||||||
As Wasp grows further, it should allow the development of web apps of increasing complexity!
|
|
||||||
|
|
||||||
While the idea is to support multiple web tech stacks in the future, right now we are focusing on the specific stack: React + react-query, NodeJS + ExpressJS, Prisma. We might yet change that as time goes on, taking trends into account, but for now, this is serving us well to develop Wasp.
|
While the idea is to support multiple web tech stacks in the future, right now we are focusing on the specific stack: React + react-query, NodeJS + ExpressJS, Prisma.
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
|
@ -1,5 +1,23 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.11.1
|
||||||
|
|
||||||
|
### 🎉 [New feature] Prisma client preview flags
|
||||||
|
Wasp now allows you to enable desired `previewFeatures` for the Prisma client:
|
||||||
|
```
|
||||||
|
app MyApp {
|
||||||
|
title: "My app",
|
||||||
|
// ...
|
||||||
|
db: {
|
||||||
|
// ...
|
||||||
|
prisma: {
|
||||||
|
clientPreviewFeatures: ["extendedWhereUnique"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Read all about Prisma preview features in [the official docs](https://www.prisma.io/docs/concepts/components/preview-features/client-preview-features).
|
||||||
|
|
||||||
## v0.11.0
|
## v0.11.0
|
||||||
|
|
||||||
### 🎉 Big new features 🎉
|
### 🎉 Big new features 🎉
|
||||||
|
@ -207,11 +207,9 @@ alias wrun="/home/martin/git/wasp-lang/wasp/waspc/run"
|
|||||||
### Typescript packages
|
### Typescript packages
|
||||||
Wasp bundles some TypeScript packages into the installation artifact (eg: deployment scripts), which end up in the installed version's `waspc_datadir`. To do so in CI, it runs `./tools/install_packages_to_data_dir.sh`.
|
Wasp bundles some TypeScript packages into the installation artifact (eg: deployment scripts), which end up in the installed version's `waspc_datadir`. To do so in CI, it runs `./tools/install_packages_to_data_dir.sh`.
|
||||||
|
|
||||||
During normal local development you can treat the packages in `packages/` as
|
`waspc`, while implemented in Haskell, for some of its functionality on TypeScript code (e.g. for parsing TS code, or for deployment scripts). In these cases, the Haskell code runs these TS packages as separate processes and communicates through input/output streams. These packages are located in `packages/` and are normal npm projects. See `packages/README.md` for how the projects are expected to be set up.
|
||||||
regular npm projects. See `packages/README.md` for specific information as to
|
|
||||||
how these projectss are expected to be set up. However, if you want to test it as part of the Wasp CLI, you can make use of this same script locally. Just manually invoke it before you run something like `cabal run wasp-cli deploy fly ...` in a wasp project so the local data directory is up to date.
|
|
||||||
|
|
||||||
Note that you can not test these packages as part of `waspc` with `cabal install`: cabal does not copy `packages` along with the rest of the data directory due to a limitation in how you tell cabal which data files to include.
|
To make these packages available to `waspc` in development, run `./run wasp-packages:compile`. When any changes are made to these packages, run the same command again.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
For tests we are using [**Tasty**](https://github.com/UnkindPartition/tasty) testing framework. Tasty let's us combine different types of tests into a single test suite.
|
For tests we are using [**Tasty**](https://github.com/UnkindPartition/tasty) testing framework. Tasty let's us combine different types of tests into a single test suite.
|
||||||
|
@ -8,6 +8,9 @@ datasource db {
|
|||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
output = {=& prismaClientOutputDir =}
|
output = {=& prismaClientOutputDir =}
|
||||||
|
{=# prismaPreviewFeatures =}
|
||||||
|
previewFeatures = {=& . =}
|
||||||
|
{=/ prismaPreviewFeatures =}
|
||||||
}
|
}
|
||||||
|
|
||||||
{=# modelSchemas =}
|
{=# modelSchemas =}
|
||||||
|
@ -16,7 +16,7 @@ export function getLoginRoute({
|
|||||||
|
|
||||||
args.email = args.email.toLowerCase()
|
args.email = args.email.toLowerCase()
|
||||||
|
|
||||||
const user = await findUserBy<'email'>({ email: args.email })
|
const user = await findUserBy({ email: args.email })
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throwInvalidCredentialsError()
|
throwInvalidCredentialsError()
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ export function getRequestPasswordResetRoute({
|
|||||||
|
|
||||||
args.email = args.email.toLowerCase();
|
args.email = args.email.toLowerCase();
|
||||||
|
|
||||||
const user = await findUserBy<'email'>({ email: args.email });
|
const user = await findUserBy({ email: args.email });
|
||||||
|
|
||||||
// User not found or not verified - don't leak information
|
// User not found or not verified - don't leak information
|
||||||
if (!user || !user.isEmailVerified) {
|
if (!user || !user.isEmailVerified) {
|
||||||
|
@ -12,7 +12,7 @@ export async function resetPassword(
|
|||||||
const { token, password } = args;
|
const { token, password } = args;
|
||||||
try {
|
try {
|
||||||
const { id: userId } = await verifyToken(token);
|
const { id: userId } = await verifyToken(token);
|
||||||
const user = await findUserBy<'id'>({ id: userId });
|
const user = await findUserBy({ id: userId });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(400).json({ success: false, message: 'Invalid token' });
|
return res.status(400).json({ success: false, message: 'Invalid token' });
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ export function getSignupRoute({
|
|||||||
|
|
||||||
userFields.email = userFields.email.toLowerCase();
|
userFields.email = userFields.email.toLowerCase();
|
||||||
|
|
||||||
const existingUser = await findUserBy<'email'>({ email: userFields.email });
|
const existingUser = await findUserBy({ email: userFields.email });
|
||||||
// User already exists and is verified - don't leak information
|
// User already exists and is verified - don't leak information
|
||||||
if (existingUser && existingUser.isEmailVerified) {
|
if (existingUser && existingUser.isEmailVerified) {
|
||||||
await doFakeWork();
|
await doFakeWork();
|
||||||
|
@ -7,7 +7,7 @@ import { findUserBy, createAuthToken } from '../../utils.js'
|
|||||||
export default handleRejection(async (req, res) => {
|
export default handleRejection(async (req, res) => {
|
||||||
const args = req.body || {}
|
const args = req.body || {}
|
||||||
|
|
||||||
const user = await findUserBy<'username'>({ username: args.username })
|
const user = await findUserBy({ username: args.username })
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throwInvalidCredentialsError()
|
throwInvalidCredentialsError()
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ export const authConfig = {
|
|||||||
successRedirectPath: "{= successRedirectPath =}",
|
successRedirectPath: "{= successRedirectPath =}",
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findUserBy<K extends keyof {= userEntityUpper =}>(where: { [key in K]: {= userEntityUpper =}[K] }): Promise<{= userEntityUpper =}> {
|
export async function findUserBy(where: Prisma.{= userEntityUpper =}WhereUniqueInput): Promise<{= userEntityUpper =}> {
|
||||||
return prisma.{= userEntityLower =}.findUnique({ where });
|
return prisma.{= userEntityLower =}.findUnique({ where });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
app waspBuild {
|
app waspBuild {
|
||||||
db: { system: PostgreSQL },
|
db: { system: PostgreSQL },
|
||||||
wasp: {
|
wasp: {
|
||||||
version: "^0.11.0"
|
version: "^0.11.1"
|
||||||
},
|
},
|
||||||
title: "waspBuild"
|
title: "waspBuild"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
app waspCompile {
|
app waspCompile {
|
||||||
wasp: {
|
wasp: {
|
||||||
version: "^0.11.0"
|
version: "^0.11.1"
|
||||||
},
|
},
|
||||||
title: "waspCompile"
|
title: "waspCompile"
|
||||||
}
|
}
|
||||||
|
@ -186,7 +186,7 @@
|
|||||||
"file",
|
"file",
|
||||||
"server/src/auth/utils.ts"
|
"server/src/auth/utils.ts"
|
||||||
],
|
],
|
||||||
"4b57bc76321dbc5708ae28aeffb6e6402e06517221e2044cf3712b16e124a4ff"
|
"b611de9a6b546f6f1cec4497a4cb525150399131dfba32b53ba34afb04e62e96"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
|
@ -20,7 +20,7 @@ export const authConfig = {
|
|||||||
successRedirectPath: "/",
|
successRedirectPath: "/",
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findUserBy<K extends keyof User>(where: { [key in K]: User[K] }): Promise<User> {
|
export async function findUserBy(where: Prisma.UserWhereUniqueInput): Promise<User> {
|
||||||
return prisma.user.findUnique({ where });
|
return prisma.user.findUnique({ where });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
app waspComplexTest {
|
app waspComplexTest {
|
||||||
db: { system: PostgreSQL },
|
db: { system: PostgreSQL },
|
||||||
wasp: {
|
wasp: {
|
||||||
version: "^0.11.0"
|
version: "^0.11.1"
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
userEntity: User,
|
userEntity: User,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
app waspJob {
|
app waspJob {
|
||||||
db: { system: PostgreSQL },
|
db: { system: PostgreSQL },
|
||||||
wasp: {
|
wasp: {
|
||||||
version: "^0.11.0"
|
version: "^0.11.1"
|
||||||
},
|
},
|
||||||
title: "waspJob"
|
title: "waspJob"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
app waspMigrate {
|
app waspMigrate {
|
||||||
wasp: {
|
wasp: {
|
||||||
version: "^0.11.0"
|
version: "^0.11.1"
|
||||||
},
|
},
|
||||||
title: "waspMigrate"
|
title: "waspMigrate"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
app waspNew {
|
app waspNew {
|
||||||
wasp: {
|
wasp: {
|
||||||
version: "^0.11.0"
|
version: "^0.11.1"
|
||||||
},
|
},
|
||||||
title: "waspNew"
|
title: "waspNew"
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,8 @@ packages/<package-name>/package-lock.json
|
|||||||
packages/<package-name>/dist/**/*.js
|
packages/<package-name>/dist/**/*.js
|
||||||
```
|
```
|
||||||
|
|
||||||
The last line assumes the project is compiled to JavaScript files inside the
|
The last line assumes the project is compiled to `.js` files inside the `dist`
|
||||||
`dist` directory. You should adjust that if needed.
|
directory. You should adjust this and/or add more file extensions if needed.
|
||||||
|
|
||||||
# CI Builds/Release
|
# CI Builds/Release
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
NOTE: `typescript` is purposefully a normal dependency instead of a dev
|
This package provides a command-line interface for getting information about
|
||||||
dependency.
|
exported symbols from JS/TS files. As input you give it a list of exports requests,
|
||||||
|
each containing a list of filepaths and, optionally, a path to a tsconfig file.
|
||||||
Run the program `node ./dist/index.js` and pass a list of export requests over
|
|
||||||
stdin:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
@ -14,7 +12,12 @@ stdin:
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
It will respond with an object mapping filenames to exports, something like:
|
Note that an instance of the TypeScript compiler is created for each exports
|
||||||
|
request, so grouping all files with the same tsconfig into one request confers
|
||||||
|
some performance benefit.
|
||||||
|
|
||||||
|
The program responds with a list of exports for each file. The filepaths in the
|
||||||
|
result will always exactly match the filepaths in the input:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
@ -11,6 +11,10 @@
|
|||||||
"start": "node ./dist/index.js",
|
"start": "node ./dist/index.js",
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
|
"//": [
|
||||||
|
"typescript is purposefully a normal dependency, and not a dev dependency:",
|
||||||
|
"the compiler is called at runtime to extract information from JS/TS files."
|
||||||
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"typescript": "^5.1.3",
|
"typescript": "^5.1.3",
|
||||||
|
@ -4,14 +4,14 @@ import * as path from 'path';
|
|||||||
import JSON5 from 'json5';
|
import JSON5 from 'json5';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const ExportRequest = z.object({
|
export const ExportsRequest = z.object({
|
||||||
tsconfig: z.string().optional(),
|
tsconfig: z.string().optional(),
|
||||||
filenames: z.array(z.string())
|
filepaths: z.array(z.string())
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ExportRequests = z.array(ExportRequest);
|
export const ExportsRequests = z.array(ExportsRequest);
|
||||||
|
|
||||||
export type ExportRequest = z.infer<typeof ExportRequest>;
|
export type ExportsRequest = z.infer<typeof ExportsRequest>;
|
||||||
|
|
||||||
export type Export
|
export type Export
|
||||||
= { type: 'default' } & Range
|
= { type: 'default' } & Range
|
||||||
@ -21,33 +21,24 @@ export type Range = { range?: { start: Location, end: Location } }
|
|||||||
|
|
||||||
export type Location = { line: number, column: number }
|
export type Location = { line: number, column: number }
|
||||||
|
|
||||||
export async function getExportsOfFiles(request: ExportRequest): Promise<{ [file: string]: Export[] }> {
|
export async function getExportsOfFiles(request: ExportsRequest): Promise<{ [file: string]: Export[] }> {
|
||||||
let compilerOptions: ts.CompilerOptions = {};
|
let compilerOptions: ts.CompilerOptions = {};
|
||||||
|
|
||||||
// If a tsconfig is given, load the configuration.
|
// If a tsconfig is given, load the configuration.
|
||||||
if (request.tsconfig) {
|
if (request.tsconfig) {
|
||||||
const configJson = JSON5.parse(await fs.readFile(request.tsconfig, 'utf8'));
|
compilerOptions = await loadCompilerOptionsFromTsConfig(request.tsconfig);
|
||||||
const basePath = path.dirname(request.tsconfig)
|
|
||||||
|
|
||||||
const { options, errors } = ts.convertCompilerOptionsFromJson(
|
|
||||||
configJson.compilerOptions, basePath, request.tsconfig
|
|
||||||
);
|
|
||||||
if (errors && errors.length) {
|
|
||||||
throw errors;
|
|
||||||
}
|
|
||||||
compilerOptions = options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportsMap: { [file: string]: Export[] } = {};
|
const exportsMap: { [file: string]: Export[] } = {};
|
||||||
|
|
||||||
// Initialize the TS compiler.
|
// Initialize the TS compiler.
|
||||||
const program = ts.createProgram(request.filenames, compilerOptions);
|
const program = ts.createProgram(request.filepaths, compilerOptions);
|
||||||
const checker = program.getTypeChecker();
|
const checker = program.getTypeChecker();
|
||||||
|
|
||||||
// Loop through each given file and try to get its exports.
|
// Loop through each given file and try to get its exports.
|
||||||
for (let filename of request.filenames) {
|
for (let filename of request.filepaths) {
|
||||||
try {
|
try {
|
||||||
exportsMap[filename] = getExportsForFile(program, checker, filename);
|
exportsMap[filename] = getExportsOfFile(program, checker, filename);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
exportsMap[filename] = [];
|
exportsMap[filename] = [];
|
||||||
@ -57,7 +48,20 @@ export async function getExportsOfFiles(request: ExportRequest): Promise<{ [file
|
|||||||
return exportsMap;
|
return exportsMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExportsForFile(program: ts.Program, checker: ts.TypeChecker, filename: string): Export[] {
|
async function loadCompilerOptionsFromTsConfig(tsconfig: string): Promise<ts.CompilerOptions> {
|
||||||
|
const configJson = JSON5.parse(await fs.readFile(tsconfig, 'utf8'));
|
||||||
|
const basePath = path.dirname(tsconfig)
|
||||||
|
|
||||||
|
const { options, errors } = ts.convertCompilerOptionsFromJson(
|
||||||
|
configJson.compilerOptions, basePath, tsconfig
|
||||||
|
);
|
||||||
|
if (errors && errors.length) {
|
||||||
|
throw errors;
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExportsOfFile(program: ts.Program, checker: ts.TypeChecker, filename: string): Export[] {
|
||||||
const source = program.getSourceFile(filename);
|
const source = program.getSourceFile(filename);
|
||||||
if (!source) {
|
if (!source) {
|
||||||
throw new Error(`Error getting source for ${filename}`);
|
throw new Error(`Error getting source for ${filename}`);
|
||||||
@ -67,23 +71,26 @@ function getExportsForFile(program: ts.Program, checker: ts.TypeChecker, filenam
|
|||||||
// This is caused by errors within the TS file, so we say there are no exports.
|
// This is caused by errors within the TS file, so we say there are no exports.
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const exports = checker.getExportsOfModule(moduleSymbol);
|
const exportSymbols = checker.getExportsOfModule(moduleSymbol);
|
||||||
return exports.map(exp => getExportForExportSymbol(program, checker, exp));
|
return exportSymbols.map(exp => getExportForExportSymbol(program, checker, exp));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExportForExportSymbol(program: ts.Program, checker: ts.TypeChecker, exp: ts.Symbol): Export {
|
function getExportForExportSymbol(program: ts.Program, checker: ts.TypeChecker, exportSymbol: ts.Symbol): Export {
|
||||||
let range = undefined;
|
let range = undefined;
|
||||||
if (exp.valueDeclaration) {
|
// Try to get the location information from the first value declaration for
|
||||||
|
// the export. If there are no value declarations, we don't return any location
|
||||||
|
// info for this export and `range` remains undefined.
|
||||||
|
if (exportSymbol.valueDeclaration) {
|
||||||
// NOTE: This isn't a very robust way of getting the location: it will always
|
// NOTE: This isn't a very robust way of getting the location: it will always
|
||||||
// point to the line that has `export`, rather than the line where the exported
|
// point to the line that has `export`, rather than the line where the exported
|
||||||
// symbol is defined.
|
// symbol is defined.
|
||||||
const startOffset = exp.valueDeclaration.getStart();
|
const startOffset = exportSymbol.valueDeclaration.getStart();
|
||||||
const startPos = ts.getLineAndCharacterOfPosition(
|
const startPos = ts.getLineAndCharacterOfPosition(
|
||||||
exp.valueDeclaration.getSourceFile(), startOffset
|
exportSymbol.valueDeclaration.getSourceFile(), startOffset
|
||||||
);
|
);
|
||||||
const endOffset = exp.valueDeclaration.getEnd();
|
const endOffset = exportSymbol.valueDeclaration.getEnd();
|
||||||
const endPos = ts.getLineAndCharacterOfPosition(
|
const endPos = ts.getLineAndCharacterOfPosition(
|
||||||
exp.valueDeclaration.getSourceFile(), endOffset
|
exportSymbol.valueDeclaration.getSourceFile(), endOffset
|
||||||
)
|
)
|
||||||
range = {
|
range = {
|
||||||
start: { line: startPos.line, column: startPos.character },
|
start: { line: startPos.line, column: startPos.character },
|
||||||
@ -92,7 +99,7 @@ function getExportForExportSymbol(program: ts.Program, checker: ts.TypeChecker,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert export to the output format.
|
// Convert export to the output format.
|
||||||
const exportName = exp.getName();
|
const exportName = exportSymbol.getName();
|
||||||
if (exportName === 'default') {
|
if (exportName === 'default') {
|
||||||
return { type: 'default', range };
|
return { type: 'default', range };
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ExportRequests, getExportsOfFiles } from "./exports.js";
|
import { ExportsRequests, getExportsOfFiles } from "./exports.js";
|
||||||
|
|
||||||
async function readStdin(): Promise<string> {
|
async function readAllFromStdin(): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let chunks = '';
|
let chunks = '';
|
||||||
process.stdin.on('data', (data) => {
|
process.stdin.on('data', (data) => {
|
||||||
@ -13,9 +13,9 @@ async function readStdin(): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const inputStr = await readStdin();
|
const inputStr = await readAllFromStdin();
|
||||||
const input = JSON.parse(inputStr);
|
const input = JSON.parse(inputStr);
|
||||||
const requests = ExportRequests.parse(input);
|
const requests = ExportsRequests.parse(input);
|
||||||
|
|
||||||
let exports = {};
|
let exports = {};
|
||||||
for (let request of requests) {
|
for (let request of requests) {
|
||||||
|
@ -1,34 +1,36 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getExportsOfFiles } from "../src/exports";
|
import { getExportsOfFiles } from "../src/exports";
|
||||||
|
|
||||||
|
// TODO(before merge): run these tests in CI
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an absolute path to a test file
|
* Get an absolute path to a test file
|
||||||
* @param filename Name of test file inside __dirname/exportTests directory
|
* @param filename Name of test file inside __dirname/exportTests directory
|
||||||
*/
|
*/
|
||||||
function testFile(filename: string): string {
|
function getTestFilePath(filename: string): string {
|
||||||
return path.join(__dirname, 'exportTests', filename);
|
return path.join(__dirname, 'exportTests', filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
const testFiles = {
|
const testFiles = {
|
||||||
emptyFile: testFile('empty.ts'),
|
emptyFile: getTestFilePath('empty.ts'),
|
||||||
addFile: testFile('add.ts'),
|
addFile: getTestFilePath('add.ts'),
|
||||||
complexFile: testFile('complex.ts'),
|
complexFile: getTestFilePath('complex.ts'),
|
||||||
dictExportFile: testFile('dict_export.ts'),
|
dictExportFile: getTestFilePath('dict_export.ts'),
|
||||||
constExportFile: testFile('const_export.ts'),
|
constExportFile: getTestFilePath('const_export.ts'),
|
||||||
|
|
||||||
emptyTsconfig: testFile('tsconfig.json'),
|
emptyTsconfig: getTestFilePath('tsconfig.json'),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('exports.ts', () => {
|
describe('exports.ts', () => {
|
||||||
test('empty ts file has empty exports', async () => {
|
test('empty ts file has empty exports', async () => {
|
||||||
const request = { filenames: [testFiles.emptyFile] };
|
const request = { filepaths: [testFiles.emptyFile] };
|
||||||
expect(await getExportsOfFiles(request)).toEqual({
|
expect(await getExportsOfFiles(request)).toEqual({
|
||||||
[testFiles.emptyFile]: []
|
[testFiles.emptyFile]: []
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('add file has just a default export', async () => {
|
test('add file has just a default export', async () => {
|
||||||
const request = { filenames: [testFiles.addFile] };
|
const request = { filepaths: [testFiles.addFile] };
|
||||||
expect(await getExportsOfFiles(request)).toEqual({
|
expect(await getExportsOfFiles(request)).toEqual({
|
||||||
[testFiles.addFile]: [{
|
[testFiles.addFile]: [{
|
||||||
type: 'default',
|
type: 'default',
|
||||||
@ -41,7 +43,7 @@ describe('exports.ts', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('complex file has default and normal export', async () => {
|
test('complex file has default and normal export', async () => {
|
||||||
const request = { filenames: [testFiles.complexFile] };
|
const request = { filepaths: [testFiles.complexFile] };
|
||||||
expect(await getExportsOfFiles(request)).toEqual({
|
expect(await getExportsOfFiles(request)).toEqual({
|
||||||
[testFiles.complexFile]: [
|
[testFiles.complexFile]: [
|
||||||
{
|
{
|
||||||
@ -70,7 +72,7 @@ describe('exports.ts', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('dict_export file shows names for each export in dict', async () => {
|
test('dict_export file shows names for each export in dict', async () => {
|
||||||
const request = { filenames: [testFiles.dictExportFile] };
|
const request = { filepaths: [testFiles.dictExportFile] };
|
||||||
expect(await getExportsOfFiles(request)).toEqual({
|
expect(await getExportsOfFiles(request)).toEqual({
|
||||||
[testFiles.dictExportFile]: [
|
[testFiles.dictExportFile]: [
|
||||||
{ type: 'named', name: 'add' },
|
{ type: 'named', name: 'add' },
|
||||||
@ -80,14 +82,14 @@ describe('exports.ts', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('empty ts file works with empty tsconfig', async () => {
|
test('empty ts file works with empty tsconfig', async () => {
|
||||||
const request = { filenames: [testFiles.emptyFile], tsconfig: testFiles.emptyTsconfig };
|
const request = { filepaths: [testFiles.emptyFile], tsconfig: testFiles.emptyTsconfig };
|
||||||
expect(await getExportsOfFiles(request)).toEqual({
|
expect(await getExportsOfFiles(request)).toEqual({
|
||||||
[testFiles.emptyFile]: []
|
[testFiles.emptyFile]: []
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('`export const` shows up in export list', async () => {
|
test('`export const` shows up in export list', async () => {
|
||||||
const request = { filenames: [testFiles.constExportFile] };
|
const request = { filepaths: [testFiles.constExportFile] };
|
||||||
expect(await getExportsOfFiles(request)).toEqual({
|
expect(await getExportsOfFiles(request)).toEqual({
|
||||||
[testFiles.constExportFile]: [{
|
[testFiles.constExportFile]: [{
|
||||||
type: 'named', name: 'isEven', range: {
|
type: 'named', name: 'isEven', range: {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
module Wasp.AppSpec.App.Db
|
module Wasp.AppSpec.App.Db
|
||||||
( Db (..),
|
( Db (..),
|
||||||
DbSystem (..),
|
DbSystem (..),
|
||||||
|
PrismaOptions (..),
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
@ -11,9 +12,15 @@ import Wasp.AppSpec.ExtImport (ExtImport)
|
|||||||
|
|
||||||
data Db = Db
|
data Db = Db
|
||||||
{ system :: Maybe DbSystem,
|
{ system :: Maybe DbSystem,
|
||||||
seeds :: Maybe [ExtImport]
|
seeds :: Maybe [ExtImport],
|
||||||
|
prisma :: Maybe PrismaOptions
|
||||||
}
|
}
|
||||||
deriving (Show, Eq, Data)
|
deriving (Show, Eq, Data)
|
||||||
|
|
||||||
data DbSystem = PostgreSQL | SQLite
|
data DbSystem = PostgreSQL | SQLite
|
||||||
deriving (Show, Eq, Data)
|
deriving (Show, Eq, Data)
|
||||||
|
|
||||||
|
data PrismaOptions = PrismaOptions
|
||||||
|
{ clientPreviewFeatures :: Maybe [String]
|
||||||
|
}
|
||||||
|
deriving (Show, Eq, Data)
|
||||||
|
@ -67,7 +67,8 @@ genPrismaSchema spec = do
|
|||||||
[ "modelSchemas" .= map entityToPslModelSchema (AS.getDecls @AS.Entity.Entity spec),
|
[ "modelSchemas" .= map entityToPslModelSchema (AS.getDecls @AS.Entity.Entity spec),
|
||||||
"datasourceProvider" .= datasourceProvider,
|
"datasourceProvider" .= datasourceProvider,
|
||||||
"datasourceUrl" .= datasourceUrl,
|
"datasourceUrl" .= datasourceUrl,
|
||||||
"prismaClientOutputDir" .= makeEnvVarField Wasp.Generator.DbGenerator.Common.prismaClientOutputDirEnvVar
|
"prismaClientOutputDir" .= makeEnvVarField Wasp.Generator.DbGenerator.Common.prismaClientOutputDirEnvVar,
|
||||||
|
"prismaPreviewFeatures" .= prismaPreviewFeatures
|
||||||
]
|
]
|
||||||
|
|
||||||
return $ createTemplateFileDraft Wasp.Generator.DbGenerator.Common.dbSchemaFileInProjectRootDir tmplSrcPath (Just templateData)
|
return $ createTemplateFileDraft Wasp.Generator.DbGenerator.Common.dbSchemaFileInProjectRootDir tmplSrcPath (Just templateData)
|
||||||
@ -75,6 +76,7 @@ genPrismaSchema spec = do
|
|||||||
tmplSrcPath = Wasp.Generator.DbGenerator.Common.dbTemplatesDirInTemplatesDir </> Wasp.Generator.DbGenerator.Common.dbSchemaFileInDbTemplatesDir
|
tmplSrcPath = Wasp.Generator.DbGenerator.Common.dbTemplatesDirInTemplatesDir </> Wasp.Generator.DbGenerator.Common.dbSchemaFileInDbTemplatesDir
|
||||||
dbSystem = fromMaybe AS.Db.SQLite $ AS.Db.system =<< AS.App.db (snd $ getApp spec)
|
dbSystem = fromMaybe AS.Db.SQLite $ AS.Db.system =<< AS.App.db (snd $ getApp spec)
|
||||||
makeEnvVarField envVarName = "env(\"" ++ envVarName ++ "\")"
|
makeEnvVarField envVarName = "env(\"" ++ envVarName ++ "\")"
|
||||||
|
prismaPreviewFeatures = show <$> (AS.Db.clientPreviewFeatures =<< AS.Db.prisma =<< AS.App.db (snd $ getApp spec))
|
||||||
|
|
||||||
entityToPslModelSchema :: (String, AS.Entity.Entity) -> String
|
entityToPslModelSchema :: (String, AS.Entity.Entity) -> String
|
||||||
entityToPslModelSchema (entityName, entity) =
|
entityToPslModelSchema (entityName, entity) =
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
{-# LANGUAGE DeriveAnyClass #-}
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
|
||||||
module Wasp.Package
|
module Wasp.NodePackageFFI
|
||||||
( Package (..),
|
( -- * Node Package FFI
|
||||||
getPackageProc,
|
|
||||||
|
-- Provides utilities for setting up and running node processes from the
|
||||||
|
-- @packages/@ directory.
|
||||||
|
Package (..),
|
||||||
|
getPackageProcessOptions,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
@ -51,10 +55,11 @@ scriptInPackageDir = [relfile|dist/index.js|]
|
|||||||
-- These packages are built during CI/locally via the @tools/install_packages_to_data_dir.sh@
|
-- These packages are built during CI/locally via the @tools/install_packages_to_data_dir.sh@
|
||||||
-- script.
|
-- script.
|
||||||
--
|
--
|
||||||
-- If the package does not have its dependencies installed yet (i.e. after they
|
-- If the package does not have its dependencies installed yet (for example,
|
||||||
-- just installed a Wasp version), we install the dependencies.
|
-- when the package is run for the first time after installing Wasp), we install
|
||||||
getPackageProc :: Package -> [String] -> IO P.CreateProcess
|
-- the dependencies.
|
||||||
getPackageProc package args = do
|
getPackageProcessOptions :: Package -> [String] -> IO P.CreateProcess
|
||||||
|
getPackageProcessOptions package args = do
|
||||||
getAndCheckNodeVersion >>= \case
|
getAndCheckNodeVersion >>= \case
|
||||||
Right _ -> pure ()
|
Right _ -> pure ()
|
||||||
Left errorMsg -> do
|
Left errorMsg -> do
|
||||||
@ -64,7 +69,7 @@ getPackageProc package args = do
|
|||||||
packageDir <- getPackageDir package
|
packageDir <- getPackageDir package
|
||||||
let scriptFile = packageDir </> scriptInPackageDir
|
let scriptFile = packageDir </> scriptInPackageDir
|
||||||
ensurePackageDependenciesAreInstalled packageDir
|
ensurePackageDependenciesAreInstalled packageDir
|
||||||
return $ packageProc packageDir "node" (fromAbsFile scriptFile : args)
|
return $ packageCreateProcess packageDir "node" (fromAbsFile scriptFile : args)
|
||||||
|
|
||||||
getPackageDir :: Package -> IO (Path' Abs (Dir PackageDir))
|
getPackageDir :: Package -> IO (Path' Abs (Dir PackageDir))
|
||||||
getPackageDir package = do
|
getPackageDir package = do
|
||||||
@ -76,7 +81,7 @@ getPackageDir package = do
|
|||||||
ensurePackageDependenciesAreInstalled :: Path' Abs (Dir PackageDir) -> IO ()
|
ensurePackageDependenciesAreInstalled :: Path' Abs (Dir PackageDir) -> IO ()
|
||||||
ensurePackageDependenciesAreInstalled packageDir =
|
ensurePackageDependenciesAreInstalled packageDir =
|
||||||
unlessM nodeModulesDirExists $ do
|
unlessM nodeModulesDirExists $ do
|
||||||
let npmInstallCreateProcess = packageProc packageDir "npm" ["install"]
|
let npmInstallCreateProcess = packageCreateProcess packageDir "npm" ["install"]
|
||||||
(exitCode, _out, err) <- P.readCreateProcessWithExitCode npmInstallCreateProcess ""
|
(exitCode, _out, err) <- P.readCreateProcessWithExitCode npmInstallCreateProcess ""
|
||||||
case exitCode of
|
case exitCode of
|
||||||
ExitFailure _ -> do
|
ExitFailure _ -> do
|
||||||
@ -92,9 +97,9 @@ ensurePackageDependenciesAreInstalled packageDir =
|
|||||||
--
|
--
|
||||||
-- NOTE: do not export this function! users of this module should have to go
|
-- NOTE: do not export this function! users of this module should have to go
|
||||||
-- through 'getPackageProc', which makes sure node_modules are present.
|
-- through 'getPackageProc', which makes sure node_modules are present.
|
||||||
packageProc ::
|
packageCreateProcess ::
|
||||||
Path' Abs (Dir PackageDir) ->
|
Path' Abs (Dir PackageDir) ->
|
||||||
String ->
|
String ->
|
||||||
[String] ->
|
[String] ->
|
||||||
P.CreateProcess
|
P.CreateProcess
|
||||||
packageProc packageDir cmd args = (P.proc cmd args) {P.cwd = Just $ fromAbsDir packageDir}
|
packageCreateProcess packageDir cmd args = (P.proc cmd args) {P.cwd = Just $ fromAbsDir packageDir}
|
@ -11,7 +11,7 @@ import StrongPath (Abs, Dir, Path', relfile, toFilePath, (</>))
|
|||||||
import System.Directory (doesFileExist)
|
import System.Directory (doesFileExist)
|
||||||
import System.Exit (ExitCode (..))
|
import System.Exit (ExitCode (..))
|
||||||
import qualified System.Process as P
|
import qualified System.Process as P
|
||||||
import Wasp.Package (Package (DeployPackage), getPackageProc)
|
import Wasp.NodePackageFFI (Package (DeployPackage), getPackageProcessOptions)
|
||||||
import Wasp.Project.Common (WaspProjectDir)
|
import Wasp.Project.Common (WaspProjectDir)
|
||||||
|
|
||||||
loadUserDockerfileContents :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe Text)
|
loadUserDockerfileContents :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe Text)
|
||||||
@ -28,7 +28,7 @@ deploy ::
|
|||||||
IO (Either String ())
|
IO (Either String ())
|
||||||
deploy waspExe waspDir cmdArgs = do
|
deploy waspExe waspDir cmdArgs = do
|
||||||
let deployScriptArgs = concat [cmdArgs, ["--wasp-exe", waspExe, "--wasp-project-dir", toFilePath waspDir]]
|
let deployScriptArgs = concat [cmdArgs, ["--wasp-exe", waspExe, "--wasp-project-dir", toFilePath waspDir]]
|
||||||
cp <- getPackageProc DeployPackage deployScriptArgs
|
cp <- getPackageProcessOptions DeployPackage deployScriptArgs
|
||||||
-- Set up the process so that it:
|
-- Set up the process so that it:
|
||||||
-- - Inherits handles from the waspc process (it will print and read from stdin/out/err)
|
-- - Inherits handles from the waspc process (it will print and read from stdin/out/err)
|
||||||
-- - Delegates Ctrl+C: when waspc receives Ctrl+C while this process is running,
|
-- - Delegates Ctrl+C: when waspc receives Ctrl+C while this process is running,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{-# LANGUAGE DeriveGeneric #-}
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
|
|
||||||
module Wasp.TypeScript
|
module Wasp.TypeScript.Inspect.Exports
|
||||||
( -- * Getting Information About TypeScript Files
|
( -- * Getting Information About TypeScript Files
|
||||||
|
|
||||||
-- Internally, this module calls out to @packages/ts-inspect@, which uses
|
-- Internally, this module calls out to @packages/ts-inspect@, which uses
|
||||||
@ -12,8 +12,8 @@ module Wasp.TypeScript
|
|||||||
|
|
||||||
-- * Export lists
|
-- * Export lists
|
||||||
getExportsOfTsFiles,
|
getExportsOfTsFiles,
|
||||||
TsExportRequest (..),
|
TsExportsRequest (..),
|
||||||
TsExportResponse (..),
|
TsExportsResponse (..),
|
||||||
TsExport (..),
|
TsExport (..),
|
||||||
tsExportSourceRegion,
|
tsExportSourceRegion,
|
||||||
)
|
)
|
||||||
@ -28,16 +28,16 @@ import qualified System.Process as P
|
|||||||
import Wasp.Analyzer (SourcePosition)
|
import Wasp.Analyzer (SourcePosition)
|
||||||
import Wasp.Analyzer.Parser.SourcePosition (SourcePosition (SourcePosition))
|
import Wasp.Analyzer.Parser.SourcePosition (SourcePosition (SourcePosition))
|
||||||
import Wasp.Analyzer.Parser.SourceRegion (SourceRegion (SourceRegion))
|
import Wasp.Analyzer.Parser.SourceRegion (SourceRegion (SourceRegion))
|
||||||
import Wasp.Package (Package (TsInspectPackage), getPackageProc)
|
import Wasp.NodePackageFFI (Package (TsInspectPackage), getPackageProcessOptions)
|
||||||
|
|
||||||
-- | Attempt to get list of exported names from TypeScript files.
|
-- | Attempt to get list of exported names from TypeScript files.
|
||||||
--
|
--
|
||||||
-- The 'FilePath's in the response are guaranteed to exactly match the
|
-- The 'FilePath's in the response are guaranteed to exactly match the
|
||||||
-- corresponding 'FilePath' in the request.
|
-- corresponding 'FilePath' in the request.
|
||||||
getExportsOfTsFiles :: [TsExportRequest] -> IO (Either String TsExportResponse)
|
getExportsOfTsFiles :: [TsExportsRequest] -> IO (Either String TsExportsResponse)
|
||||||
getExportsOfTsFiles requests = do
|
getExportsOfTsFiles requests = do
|
||||||
let requestJSON = BS.toString $ encode $ groupExportRequests requests
|
let requestJSON = BS.toString $ encode $ groupExportRequestsByTsconfig requests
|
||||||
cp <- getPackageProc TsInspectPackage []
|
cp <- getPackageProcessOptions TsInspectPackage []
|
||||||
(exitCode, response, err) <- P.readCreateProcessWithExitCode cp requestJSON
|
(exitCode, response, err) <- P.readCreateProcessWithExitCode cp requestJSON
|
||||||
case exitCode of
|
case exitCode of
|
||||||
ExitSuccess -> case decode $ BS.fromString response of
|
ExitSuccess -> case decode $ BS.fromString response of
|
||||||
@ -45,15 +45,15 @@ getExportsOfTsFiles requests = do
|
|||||||
Just exports -> return $ Right exports
|
Just exports -> return $ Right exports
|
||||||
_ -> return $ Left err
|
_ -> return $ Left err
|
||||||
|
|
||||||
-- | Join export requests that have the same tsconfig. The @ts-inspect@ package
|
-- | Join exports requests that have the same tsconfig. The @ts-inspect@ package
|
||||||
-- runs an instance of the TypeScript compiler per request group, so grouping
|
-- runs an instance of the TypeScript compiler per request group, so grouping
|
||||||
-- them this way improves performance.
|
-- them this way improves performance.
|
||||||
groupExportRequests :: [TsExportRequest] -> [TsExportRequest]
|
groupExportRequestsByTsconfig :: [TsExportsRequest] -> [TsExportsRequest]
|
||||||
groupExportRequests requests =
|
groupExportRequestsByTsconfig requests =
|
||||||
map (uncurry $ flip TsExportRequest) $
|
map (uncurry $ flip TsExportsRequest) $
|
||||||
M.toList $ foldr insertRequest M.empty requests
|
M.toList $ foldr insertRequest M.empty requests
|
||||||
where
|
where
|
||||||
insertRequest (TsExportRequest names maybeTsconfig) grouped =
|
insertRequest (TsExportsRequest names maybeTsconfig) grouped =
|
||||||
M.insertWith (++) maybeTsconfig names grouped
|
M.insertWith (++) maybeTsconfig names grouped
|
||||||
|
|
||||||
-- | A symbol exported from a TypeScript file.
|
-- | A symbol exported from a TypeScript file.
|
||||||
@ -80,15 +80,15 @@ instance FromJSON TsExport where
|
|||||||
(_ :: Value) -> fail "invalid type for TsExport"
|
(_ :: Value) -> fail "invalid type for TsExport"
|
||||||
|
|
||||||
-- | Map from TypeScript files to the list of exports found in that file.
|
-- | Map from TypeScript files to the list of exports found in that file.
|
||||||
newtype TsExportResponse = TsExportResponse (M.HashMap FilePath [TsExport])
|
newtype TsExportsResponse = TsExportsResponse (M.HashMap FilePath [TsExport])
|
||||||
deriving (Eq, Show, FromJSON)
|
deriving (Eq, Show, FromJSON)
|
||||||
|
|
||||||
-- | A list of files associated with an optional tsconfig file that is run
|
-- | A list of files associated with an optional tsconfig file that is run
|
||||||
-- through the TypeScript compiler as a group.
|
-- through the TypeScript compiler as a group.
|
||||||
data TsExportRequest = TsExportRequest {filenames :: ![FilePath], tsconfig :: !(Maybe FilePath)}
|
data TsExportsRequest = TsExportsRequest {filepaths :: ![FilePath], tsconfig :: !(Maybe FilePath)}
|
||||||
deriving (Eq, Show, Generic)
|
deriving (Eq, Show, Generic)
|
||||||
|
|
||||||
instance ToJSON TsExportRequest where
|
instance ToJSON TsExportsRequest where
|
||||||
toEncoding = genericToEncoding defaultOptions
|
toEncoding = genericToEncoding defaultOptions
|
||||||
|
|
||||||
-- Wrapper types for parsing SourceRegions from data with 0-based offsets.
|
-- Wrapper types for parsing SourceRegions from data with 0-based offsets.
|
@ -63,7 +63,10 @@ spec_Analyzer = do
|
|||||||
" },",
|
" },",
|
||||||
" db: {",
|
" db: {",
|
||||||
" system: PostgreSQL,",
|
" system: PostgreSQL,",
|
||||||
" seeds: [ import { devSeedSimple } from \"@server/dbSeeds.js\" ]",
|
" seeds: [ import { devSeedSimple } from \"@server/dbSeeds.js\" ],",
|
||||||
|
" prisma: {",
|
||||||
|
" clientPreviewFeatures: [\"extendedWhereUnique\"]",
|
||||||
|
" }",
|
||||||
" },",
|
" },",
|
||||||
" emailSender: {",
|
" emailSender: {",
|
||||||
" provider: SendGrid,",
|
" provider: SendGrid,",
|
||||||
@ -174,7 +177,12 @@ spec_Analyzer = do
|
|||||||
[ ExtImport
|
[ ExtImport
|
||||||
(ExtImportField "devSeedSimple")
|
(ExtImportField "devSeedSimple")
|
||||||
(fromJust $ SP.parseRelFileP "dbSeeds.js")
|
(fromJust $ SP.parseRelFileP "dbSeeds.js")
|
||||||
]
|
],
|
||||||
|
Db.prisma =
|
||||||
|
Just
|
||||||
|
Db.PrismaOptions
|
||||||
|
{ clientPreviewFeatures = Just ["extendedWhereUnique"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
App.emailSender =
|
App.emailSender =
|
||||||
Just
|
Just
|
||||||
|
@ -6,7 +6,7 @@ cabal-version: 2.4
|
|||||||
-- Consider using hpack, or maybe even hpack-dhall.
|
-- Consider using hpack, or maybe even hpack-dhall.
|
||||||
|
|
||||||
name: waspc
|
name: waspc
|
||||||
version: 0.11.0
|
version: 0.11.1
|
||||||
description: Please see the README on GitHub at <https://github.com/wasp-lang/wasp/waspc#readme>
|
description: Please see the README on GitHub at <https://github.com/wasp-lang/wasp/waspc#readme>
|
||||||
homepage: https://github.com/wasp-lang/wasp/waspc#readme
|
homepage: https://github.com/wasp-lang/wasp/waspc#readme
|
||||||
bug-reports: https://github.com/wasp-lang/wasp/issues
|
bug-reports: https://github.com/wasp-lang/wasp/issues
|
||||||
@ -329,7 +329,7 @@ library
|
|||||||
Wasp.Message
|
Wasp.Message
|
||||||
Wasp.Node.Version
|
Wasp.Node.Version
|
||||||
Wasp.NpmDependency
|
Wasp.NpmDependency
|
||||||
Wasp.Package
|
Wasp.NodePackageFFI
|
||||||
Wasp.Project
|
Wasp.Project
|
||||||
Wasp.Project.Analyze
|
Wasp.Project.Analyze
|
||||||
Wasp.Project.Common
|
Wasp.Project.Common
|
||||||
@ -346,7 +346,7 @@ library
|
|||||||
Wasp.Psl.Parser.Model
|
Wasp.Psl.Parser.Model
|
||||||
Wasp.Psl.Util
|
Wasp.Psl.Util
|
||||||
Wasp.SemanticVersion
|
Wasp.SemanticVersion
|
||||||
Wasp.TypeScript
|
Wasp.TypeScript.Inspect.Exports
|
||||||
Wasp.Util
|
Wasp.Util
|
||||||
Wasp.Util.Aeson
|
Wasp.Util.Aeson
|
||||||
Wasp.Util.Control.Monad
|
Wasp.Util.Control.Monad
|
||||||
@ -366,20 +366,25 @@ library waspls
|
|||||||
exposed-modules:
|
exposed-modules:
|
||||||
Control.Monad.Log
|
Control.Monad.Log
|
||||||
Control.Monad.Log.Class
|
Control.Monad.Log.Class
|
||||||
Wasp.LSP.Debouncer
|
Wasp.LSP.Analysis
|
||||||
Wasp.LSP.Server
|
|
||||||
Wasp.LSP.ServerState
|
|
||||||
Wasp.LSP.ServerConfig
|
|
||||||
Wasp.LSP.ServerM
|
|
||||||
Wasp.LSP.ExtImport
|
|
||||||
Wasp.LSP.Handlers
|
|
||||||
Wasp.LSP.Diagnostic
|
|
||||||
Wasp.LSP.Completion
|
Wasp.LSP.Completion
|
||||||
Wasp.LSP.GotoDefinition
|
|
||||||
Wasp.LSP.Reactor
|
|
||||||
Wasp.LSP.Completions.Common
|
Wasp.LSP.Completions.Common
|
||||||
Wasp.LSP.Completions.DictKeyCompletion
|
Wasp.LSP.Completions.DictKeyCompletion
|
||||||
Wasp.LSP.Completions.ExprCompletion
|
Wasp.LSP.Completions.ExprCompletion
|
||||||
|
Wasp.LSP.Debouncer
|
||||||
|
Wasp.LSP.Diagnostic
|
||||||
|
Wasp.LSP.DynamicHandlers
|
||||||
|
Wasp.LSP.ExtImport.Diagnostic
|
||||||
|
Wasp.LSP.ExtImport.ExportsCache
|
||||||
|
Wasp.LSP.ExtImport.Path
|
||||||
|
Wasp.LSP.ExtImport.Syntax
|
||||||
|
Wasp.LSP.GotoDefinition
|
||||||
|
Wasp.LSP.Handlers
|
||||||
|
Wasp.LSP.Reactor
|
||||||
|
Wasp.LSP.Server
|
||||||
|
Wasp.LSP.ServerConfig
|
||||||
|
Wasp.LSP.ServerMonads
|
||||||
|
Wasp.LSP.ServerState
|
||||||
Wasp.LSP.SignatureHelp
|
Wasp.LSP.SignatureHelp
|
||||||
Wasp.LSP.Syntax
|
Wasp.LSP.Syntax
|
||||||
Wasp.LSP.TypeInference
|
Wasp.LSP.TypeInference
|
||||||
|
102
waspc/waspls/src/Wasp/LSP/Analysis.hs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
module Wasp.LSP.Analysis
|
||||||
|
( diagnoseWaspFile,
|
||||||
|
publishDiagnostics,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Lens ((.~), (?~), (^.))
|
||||||
|
import Control.Monad (when)
|
||||||
|
import Control.Monad.Log.Class (logM)
|
||||||
|
import Control.Monad.Reader.Class (asks)
|
||||||
|
import qualified Data.HashMap.Strict as M
|
||||||
|
import Data.Maybe (isJust)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Language.LSP.Server as LSP
|
||||||
|
import qualified Language.LSP.Types as LSP
|
||||||
|
import qualified Language.LSP.VFS as LSP
|
||||||
|
import Wasp.Analyzer (analyze)
|
||||||
|
import Wasp.Analyzer.Parser.ConcreteParser (parseCST)
|
||||||
|
import qualified Wasp.Analyzer.Parser.Lexer as L
|
||||||
|
import Wasp.LSP.Debouncer (debounce)
|
||||||
|
import Wasp.LSP.Diagnostic (WaspDiagnostic (AnalyzerDiagnostic, ParseDiagnostic), waspDiagnosticToLspDiagnostic)
|
||||||
|
import Wasp.LSP.ExtImport.Diagnostic (updateMissingExtImportDiagnostics)
|
||||||
|
import Wasp.LSP.ExtImport.ExportsCache (refreshExportsForAllExtImports)
|
||||||
|
import Wasp.LSP.ServerMonads (HandlerM, ServerM, handler, modify, sendToReactor)
|
||||||
|
import qualified Wasp.LSP.ServerState as State
|
||||||
|
|
||||||
|
-- | Finds diagnostics on a wasp file and sends the diagnostics to the LSP
|
||||||
|
-- client.
|
||||||
|
diagnoseWaspFile :: LSP.Uri -> ServerM ()
|
||||||
|
diagnoseWaspFile uri = do
|
||||||
|
analyzeWaspFile uri
|
||||||
|
|
||||||
|
-- Immediately update import diagnostics only when file watching is enabled
|
||||||
|
sourceWatchingEnabled <- isJust <$> handler (asks (^. State.regTokens . State.watchSourceFilesToken))
|
||||||
|
when sourceWatchingEnabled updateMissingExtImportDiagnostics
|
||||||
|
|
||||||
|
-- Send diagnostics to client
|
||||||
|
handler $ publishDiagnostics uri
|
||||||
|
|
||||||
|
-- Update exports and missing import diagnostics asynchronously. This is only
|
||||||
|
-- done if file watching is NOT enabled or if the export cache hasn't been
|
||||||
|
-- filled before.
|
||||||
|
exportCacheIsEmpty <- M.null <$> handler (asks (^. State.tsExports))
|
||||||
|
debouncer <- handler $ asks (^. State.debouncer)
|
||||||
|
when (not sourceWatchingEnabled || exportCacheIsEmpty) $
|
||||||
|
debounce debouncer 500000 State.RefreshExports $
|
||||||
|
sendToReactor $ do
|
||||||
|
refreshExportsForAllExtImports
|
||||||
|
updateMissingExtImportDiagnostics
|
||||||
|
handler $ publishDiagnostics uri
|
||||||
|
|
||||||
|
-- | Send diagnostics stored in 'State.latestDiagnostics' to the LSP client.
|
||||||
|
publishDiagnostics :: LSP.Uri -> HandlerM ()
|
||||||
|
publishDiagnostics uri = do
|
||||||
|
currentDiagnostics <- asks (^. State.latestDiagnostics)
|
||||||
|
srcString <- asks (^. State.currentWaspSource)
|
||||||
|
let lspDiagnostics = map (waspDiagnosticToLspDiagnostic srcString) currentDiagnostics
|
||||||
|
LSP.sendNotification
|
||||||
|
LSP.STextDocumentPublishDiagnostics
|
||||||
|
$ LSP.PublishDiagnosticsParams uri Nothing (LSP.List lspDiagnostics)
|
||||||
|
|
||||||
|
-- | Run wasp Analyzer on a file and replace the diagnostics in 'State.latestDiagnostics'
|
||||||
|
-- with the diagnostics reported by the Analyzer.
|
||||||
|
analyzeWaspFile :: LSP.Uri -> ServerM ()
|
||||||
|
analyzeWaspFile uri = do
|
||||||
|
modify (State.waspFileUri ?~ uri)
|
||||||
|
|
||||||
|
-- NOTE: we have to be careful to keep CST and source string in sync at all
|
||||||
|
-- times for all threads, so we update them both atomically (via one call to
|
||||||
|
-- 'modify').
|
||||||
|
readSourceString >>= \case
|
||||||
|
Nothing -> do
|
||||||
|
logM $ "Couldn't read source from VFS for wasp file " ++ show uri
|
||||||
|
pure ()
|
||||||
|
Just srcString -> do
|
||||||
|
let (concreteErrorMessages, concreteSyntax) = parseCST $ L.lex srcString
|
||||||
|
-- Atomic update of source string and CST
|
||||||
|
modify ((State.currentWaspSource .~ srcString) . (State.cst ?~ concreteSyntax))
|
||||||
|
if not $ null concreteErrorMessages
|
||||||
|
then storeCSTErrors concreteErrorMessages
|
||||||
|
else runWaspAnalyzer srcString
|
||||||
|
where
|
||||||
|
readSourceString = fmap T.unpack <$> readVFSFile uri
|
||||||
|
|
||||||
|
storeCSTErrors concreteErrorMessages = do
|
||||||
|
let newDiagnostics = map ParseDiagnostic concreteErrorMessages
|
||||||
|
modify (State.latestDiagnostics .~ newDiagnostics)
|
||||||
|
|
||||||
|
runWaspAnalyzer srcString = do
|
||||||
|
let analyzeResult = analyze srcString
|
||||||
|
case analyzeResult of
|
||||||
|
Right _ -> do
|
||||||
|
modify (State.latestDiagnostics .~ [])
|
||||||
|
Left errs -> do
|
||||||
|
let newDiagnostics = fmap AnalyzerDiagnostic errs
|
||||||
|
modify (State.latestDiagnostics .~ newDiagnostics)
|
||||||
|
|
||||||
|
-- | Read the contents of a "Uri" in the virtual file system maintained by the
|
||||||
|
-- LSP library.
|
||||||
|
readVFSFile :: LSP.Uri -> ServerM (Maybe Text)
|
||||||
|
readVFSFile uri = fmap LSP.virtualFileText <$> LSP.getVirtualFile (LSP.toNormalizedUri uri)
|
@ -1,5 +1,15 @@
|
|||||||
module Wasp.LSP.Debouncer
|
module Wasp.LSP.Debouncer
|
||||||
( Debouncer,
|
( -- * Smoothing Noisy Events
|
||||||
|
|
||||||
|
-- Provides a debouncing API events that get triggered repeatedly in a
|
||||||
|
-- short window of time. Debouncing these events treats these repeated
|
||||||
|
-- triggers as a single firing of the event.
|
||||||
|
--
|
||||||
|
-- For example, if we want to do something when the user finishes editing
|
||||||
|
-- a file, we may look typing events. But the user will type many times
|
||||||
|
-- to make a single logical edit, so we want to consider all of those
|
||||||
|
-- individual typing events as one big typing event.
|
||||||
|
Debouncer,
|
||||||
newDebouncerIO,
|
newDebouncerIO,
|
||||||
debounce,
|
debounce,
|
||||||
)
|
)
|
||||||
@ -14,31 +24,75 @@ import Data.Foldable (traverse_)
|
|||||||
import Data.Hashable (Hashable)
|
import Data.Hashable (Hashable)
|
||||||
import qualified StmContainers.Map as STM
|
import qualified StmContainers.Map as STM
|
||||||
|
|
||||||
|
-- | @debounce debouncer waitMicros event fire@ prevents certain actions from
|
||||||
|
-- running too often by ignoring repeated attempts to run the action within a
|
||||||
|
-- certain period of time. This is done by \"debouncing\" actions with the same
|
||||||
|
-- @event@, which is a label for what is triggering this action to run.
|
||||||
|
--
|
||||||
|
-- When 'debounce' is called, it waits @waitMicros@ microseconds before running
|
||||||
|
-- the action @fire@. If 'debounce' is called again within @waitMicros@
|
||||||
|
-- microseconds, the previous action is cancelled and only @fire@ from the newer
|
||||||
|
-- call runs. The new call waits another @waitMicros@ and can also be cancelled.
|
||||||
|
--
|
||||||
|
-- Use this function to reduce the number of times some action is run. For example,
|
||||||
|
-- refreshing the list of exported symbols from TS files on file change is debounced,
|
||||||
|
-- since we don't want to do that after every key press in the editor.
|
||||||
|
--
|
||||||
|
-- ==== __Example__
|
||||||
|
-- @
|
||||||
|
-- do
|
||||||
|
-- debouncer <- newDebouncerIO
|
||||||
|
-- debounce debouncer 1000 "event a" $ putStrLn "Event A #1"
|
||||||
|
-- threadDelay 100
|
||||||
|
-- debounce debouncer 1000 "event a" $ putStrLn "Event A #2"
|
||||||
|
-- threadDelay 2000
|
||||||
|
-- @
|
||||||
|
-- Prints just @Event A #2@.
|
||||||
|
--
|
||||||
|
-- ==== __Example__
|
||||||
|
-- @
|
||||||
|
-- do
|
||||||
|
-- debouncer <- newDebouncerIO
|
||||||
|
-- debounce debouncer 1000 "event a" $ putStrLn "Event A #1"
|
||||||
|
-- threadDelay 100
|
||||||
|
-- debounce debouncer 1000 "event b" $ putStrLn "Event B #1"
|
||||||
|
-- threadDelay 2000
|
||||||
|
-- @
|
||||||
|
-- Prints both @Event A #1@ and @Event B #1@.
|
||||||
|
debounce :: (MonadUnliftIO m, MonadIO m, Eq k, Hashable k) => Debouncer k -> Int -> k -> m () -> m ()
|
||||||
|
debounce (Debouncer running) waitMicros event fire = do
|
||||||
|
fireIO <- toIO fire
|
||||||
|
|
||||||
|
-- Spawn a new thread that waits @waitMicros@ before running @fireIO@.
|
||||||
|
newDelayedAction <- liftIO $
|
||||||
|
async $ do
|
||||||
|
threadDelay waitMicros
|
||||||
|
fireIO
|
||||||
|
-- Mark this event as inactive by removing it from the running events.
|
||||||
|
atomically $ STM.delete event running
|
||||||
|
|
||||||
|
-- Atomically replace the previous action for this event (if any) with
|
||||||
|
-- @newDelayedAction@.
|
||||||
|
prevDelayedAction <- liftIO $
|
||||||
|
atomically $ do
|
||||||
|
prevAction <- STM.lookup event running
|
||||||
|
STM.insert newDelayedAction event running
|
||||||
|
return prevAction
|
||||||
|
|
||||||
|
-- Cancel the previous action for this event (if any).
|
||||||
|
liftIO $ traverse_ cancel prevDelayedAction
|
||||||
|
|
||||||
-- | Debounce events named with type @k@. Each unique @k@ (by its 'Eq' instance)
|
-- | Debounce events named with type @k@. Each unique @k@ (by its 'Eq' instance)
|
||||||
-- has its own debounce timer. Construct a debouncer with 'newDebouncerIO'.
|
-- has its own debounce timer. Construct a debouncer with 'newDebouncerIO'.
|
||||||
--
|
--
|
||||||
-- See 'debounce' for how to use it.
|
-- See 'debounce' for how to use it.
|
||||||
newtype Debouncer k = Debouncer (STM.Map k (Async ()))
|
newtype Debouncer k = Debouncer
|
||||||
|
{ -- | A thread-safe map of event labels to async actions. This stores
|
||||||
|
-- actions for each event that are waiting for their debounce timers to end.
|
||||||
|
-- This is needed so that new calls to 'debounce' can cancel the currently
|
||||||
|
-- running actions before they can finish running.
|
||||||
|
debouncerRunningEvents :: STM.Map k (Async ())
|
||||||
|
}
|
||||||
|
|
||||||
newDebouncerIO :: IO (Debouncer k)
|
newDebouncerIO :: IO (Debouncer k)
|
||||||
newDebouncerIO = Debouncer <$> STM.newIO
|
newDebouncerIO = Debouncer <$> STM.newIO
|
||||||
|
|
||||||
-- | @debounce debouncer waitMicros event action@ waits @waitMicros@ microseconds
|
|
||||||
-- and then runs @action@.
|
|
||||||
--
|
|
||||||
-- If 'debounce' is called again with the same @event@, only the newer call
|
|
||||||
-- fires.
|
|
||||||
debounce :: (MonadUnliftIO m, MonadIO m, Eq k, Hashable k) => Debouncer k -> Int -> k -> m () -> m ()
|
|
||||||
debounce (Debouncer running) waitMicros event fire = do
|
|
||||||
fireIO <- toIO fire
|
|
||||||
a <- liftIO $
|
|
||||||
async $ do
|
|
||||||
threadDelay waitMicros
|
|
||||||
fireIO
|
|
||||||
atomically $ STM.delete event running
|
|
||||||
prev <- liftIO $
|
|
||||||
atomically $ do
|
|
||||||
prev <- STM.lookup event running
|
|
||||||
STM.insert a event running
|
|
||||||
return prev
|
|
||||||
liftIO $ traverse_ cancel prev
|
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
module Wasp.LSP.Diagnostic
|
module Wasp.LSP.Diagnostic
|
||||||
( WaspDiagnostic (..),
|
( WaspDiagnostic (..),
|
||||||
MissingImportReason (..),
|
MissingExtImportReason (..),
|
||||||
waspDiagnosticToLspDiagnostic,
|
waspDiagnosticToLspDiagnostic,
|
||||||
clearMissingImportDiagnostics,
|
clearMissingExtImportDiagnostics,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import qualified Language.LSP.Types as LSP
|
import qualified Language.LSP.Types as LSP
|
||||||
import qualified StrongPath as SP
|
|
||||||
import qualified Wasp.Analyzer.AnalyzeError as W
|
import qualified Wasp.Analyzer.AnalyzeError as W
|
||||||
import qualified Wasp.Analyzer.Parser as W
|
import qualified Wasp.Analyzer.Parser as W
|
||||||
import qualified Wasp.Analyzer.Parser.ConcreteParser.ParseError as CPE
|
import qualified Wasp.Analyzer.Parser.ConcreteParser.ParseError as CPE
|
||||||
@ -17,32 +16,33 @@ import Wasp.Analyzer.Parser.Ctx (getCtxRgn)
|
|||||||
import Wasp.Analyzer.Parser.SourcePosition (SourcePosition (..), sourceOffsetToPosition)
|
import Wasp.Analyzer.Parser.SourcePosition (SourcePosition (..), sourceOffsetToPosition)
|
||||||
import Wasp.Analyzer.Parser.SourceRegion (sourceSpanToRegion)
|
import Wasp.Analyzer.Parser.SourceRegion (sourceSpanToRegion)
|
||||||
import Wasp.Analyzer.Parser.SourceSpan (SourceSpan (..))
|
import Wasp.Analyzer.Parser.SourceSpan (SourceSpan (..))
|
||||||
|
import Wasp.LSP.ExtImport.Path (WaspStyleExtFilePath (WaspStyleExtFilePath))
|
||||||
import Wasp.LSP.Util (waspSourceRegionToLspRange)
|
import Wasp.LSP.Util (waspSourceRegionToLspRange)
|
||||||
|
|
||||||
data WaspDiagnostic
|
data WaspDiagnostic
|
||||||
= ParseDiagnostic !CPE.ParseError
|
= ParseDiagnostic !CPE.ParseError
|
||||||
| AnalyzerDiagonstic !W.AnalyzeError
|
| AnalyzerDiagnostic !W.AnalyzeError
|
||||||
| MissingImportDiagnostic !SourceSpan !MissingImportReason !(SP.Path' SP.Abs SP.File')
|
| MissingExtImportDiagnostic !SourceSpan !MissingExtImportReason !WaspStyleExtFilePath
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
data MissingImportReason = NoDefaultExport | NoNamedExport !String | NoFile
|
data MissingExtImportReason = NoDefaultExport | NoNamedExport !String | NoFile
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
showMissingImportReason :: MissingImportReason -> SP.Path' SP.Abs SP.File' -> Text
|
showMissingImportReason :: MissingExtImportReason -> WaspStyleExtFilePath -> Text
|
||||||
showMissingImportReason NoDefaultExport tsFile =
|
showMissingImportReason NoDefaultExport (WaspStyleExtFilePath tsFile) =
|
||||||
"No default export in " <> Text.pack (SP.fromAbsFile tsFile)
|
"No default export in " <> Text.pack tsFile
|
||||||
showMissingImportReason (NoNamedExport name) tsFile =
|
showMissingImportReason (NoNamedExport name) (WaspStyleExtFilePath tsFile) =
|
||||||
"`" <> Text.pack name <> "` is not exported from " <> Text.pack (SP.fromAbsFile tsFile)
|
"`" <> Text.pack name <> "` is not exported from " <> Text.pack tsFile
|
||||||
showMissingImportReason NoFile tsFile =
|
showMissingImportReason NoFile (WaspStyleExtFilePath tsFile) =
|
||||||
Text.pack (SP.fromAbsFile tsFile) <> " does not exist"
|
"Module " <> Text.pack tsFile <> " does not exist"
|
||||||
|
|
||||||
missingImportSeverity :: MissingImportReason -> LSP.DiagnosticSeverity
|
missingImportSeverity :: MissingExtImportReason -> LSP.DiagnosticSeverity
|
||||||
missingImportSeverity _ = LSP.DsError
|
missingImportSeverity _ = LSP.DsError
|
||||||
|
|
||||||
waspDiagnosticToLspDiagnostic :: String -> WaspDiagnostic -> LSP.Diagnostic
|
waspDiagnosticToLspDiagnostic :: String -> WaspDiagnostic -> LSP.Diagnostic
|
||||||
waspDiagnosticToLspDiagnostic src (ParseDiagnostic err) = concreteParseErrorToDiagnostic src err
|
waspDiagnosticToLspDiagnostic src (ParseDiagnostic err) = concreteParseErrorToDiagnostic src err
|
||||||
waspDiagnosticToLspDiagnostic _ (AnalyzerDiagonstic analyzeError) = waspErrorToDiagnostic analyzeError
|
waspDiagnosticToLspDiagnostic _ (AnalyzerDiagnostic analyzeError) = waspErrorToDiagnostic analyzeError
|
||||||
waspDiagnosticToLspDiagnostic src (MissingImportDiagnostic sourceSpan reason tsFile) =
|
waspDiagnosticToLspDiagnostic src (MissingExtImportDiagnostic sourceSpan reason tsFile) =
|
||||||
let message = showMissingImportReason reason tsFile
|
let message = showMissingImportReason reason tsFile
|
||||||
severity = missingImportSeverity reason
|
severity = missingImportSeverity reason
|
||||||
region = sourceSpanToRegion src sourceSpan
|
region = sourceSpanToRegion src sourceSpan
|
||||||
@ -116,8 +116,8 @@ waspErrorRange err =
|
|||||||
let (_, W.Ctx rgn) = W.getErrorMessageAndCtx err
|
let (_, W.Ctx rgn) = W.getErrorMessageAndCtx err
|
||||||
in waspSourceRegionToLspRange rgn
|
in waspSourceRegionToLspRange rgn
|
||||||
|
|
||||||
clearMissingImportDiagnostics :: [WaspDiagnostic] -> [WaspDiagnostic]
|
clearMissingExtImportDiagnostics :: [WaspDiagnostic] -> [WaspDiagnostic]
|
||||||
clearMissingImportDiagnostics = filter (not . isMissingImportDiagnostic)
|
clearMissingExtImportDiagnostics = filter (not . isMissingImportDiagnostic)
|
||||||
where
|
where
|
||||||
isMissingImportDiagnostic (MissingImportDiagnostic _ _ _) = True
|
isMissingImportDiagnostic (MissingExtImportDiagnostic _ _ _) = True
|
||||||
isMissingImportDiagnostic _ = False
|
isMissingImportDiagnostic _ = False
|
||||||
|
116
waspc/waspls/src/Wasp/LSP/DynamicHandlers.hs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
|
||||||
|
module Wasp.LSP.DynamicHandlers
|
||||||
|
( registerDynamicCapabilities,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Lens ((.~), (^.))
|
||||||
|
import Control.Monad (forM_, (<=<))
|
||||||
|
import Control.Monad.Log.Class (logM)
|
||||||
|
import Control.Monad.Reader.Class (asks)
|
||||||
|
import Data.List (isSuffixOf, stripPrefix)
|
||||||
|
import Data.Maybe (mapMaybe)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
import qualified Language.LSP.Server as LSP
|
||||||
|
import qualified Language.LSP.Types as LSP
|
||||||
|
import qualified Language.LSP.Types.Lens as LSP
|
||||||
|
import qualified StrongPath as SP
|
||||||
|
import Wasp.LSP.Analysis (publishDiagnostics)
|
||||||
|
import Wasp.LSP.ExtImport.Diagnostic (updateMissingExtImportDiagnostics)
|
||||||
|
import Wasp.LSP.ExtImport.ExportsCache (refreshExportsOfFiles)
|
||||||
|
import Wasp.LSP.ServerMonads (ServerM, handler, modify, sendToReactor)
|
||||||
|
import qualified Wasp.LSP.ServerState as State
|
||||||
|
|
||||||
|
-- | Sends capability registration requests for all dynamic capabilities that
|
||||||
|
-- @waspls@ uses.
|
||||||
|
--
|
||||||
|
-- Dynamic capability registration comes from the LSP specification:
|
||||||
|
-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#client_registerCapability
|
||||||
|
--
|
||||||
|
-- This is different than static registration, which occurs when the server is
|
||||||
|
-- initialized: statically registered capabilities can never be unregistered and
|
||||||
|
-- only some capabilities can be statically registered.
|
||||||
|
--
|
||||||
|
-- In contrast, dynamic registration allows us to register handlers for any
|
||||||
|
-- capability, as well as unregister it. Specific to the haskell @lsp@ library
|
||||||
|
-- we use, we can also track whether the capability was successfully registered
|
||||||
|
-- only for dynamic capabilities.
|
||||||
|
--
|
||||||
|
-- In turn, clients may only support a limited set of these capabilities, so
|
||||||
|
-- the LSP allows the client to refuse to register some capabilities that waspls
|
||||||
|
-- tries to register. See each @registerX@ function for details on what
|
||||||
|
-- functionality is lost if registration for that capability fails.
|
||||||
|
registerDynamicCapabilities :: ServerM ()
|
||||||
|
registerDynamicCapabilities =
|
||||||
|
sequence_
|
||||||
|
[ registerSourceFileWatcher
|
||||||
|
]
|
||||||
|
|
||||||
|
-- | Register file watcher watcher for JS and TS files in the src/ directory.
|
||||||
|
-- When these files change, the export lists for the changed files are
|
||||||
|
-- automatically refreshed.
|
||||||
|
--
|
||||||
|
-- Note that this registration is guaranteed: if it fails, places in @waspls@ that
|
||||||
|
-- rely on up-to-date export lists need to manually refresh the export lists.
|
||||||
|
registerSourceFileWatcher :: ServerM ()
|
||||||
|
registerSourceFileWatcher = do
|
||||||
|
-- We try to watch just the @src/@ directory, but we can only specify absolute
|
||||||
|
-- glob patterns. So we can only do this when the root path is available, which
|
||||||
|
-- it practically always is after @Initialized@ is sent.
|
||||||
|
--
|
||||||
|
-- NOTE: relative glob patterns are introduced in the LSP spec 3.17, but are
|
||||||
|
-- not available in 3.16. We are limited to 3.16 because we use lsp-1.4.0.0.
|
||||||
|
let tsJsGlobPattern = "**/*.{ts,tsx,js,jsx}"
|
||||||
|
globPattern <-
|
||||||
|
LSP.getRootPath >>= \case
|
||||||
|
Nothing -> do
|
||||||
|
logM "Could not access projectRootDir when setting up source file watcher. Watching any TS/JS file instead of limiting to src/."
|
||||||
|
return tsJsGlobPattern
|
||||||
|
Just projectRootDir -> do
|
||||||
|
let srcGlobPattern = "src/" <> tsJsGlobPattern
|
||||||
|
return $
|
||||||
|
if "/" `isSuffixOf` projectRootDir
|
||||||
|
then projectRootDir <> srcGlobPattern
|
||||||
|
else projectRootDir <> "/" <> srcGlobPattern
|
||||||
|
|
||||||
|
watchSourceFilesToken <-
|
||||||
|
LSP.registerCapability
|
||||||
|
LSP.SWorkspaceDidChangeWatchedFiles
|
||||||
|
LSP.DidChangeWatchedFilesRegistrationOptions
|
||||||
|
{ _watchers =
|
||||||
|
LSP.List
|
||||||
|
[ LSP.FileSystemWatcher
|
||||||
|
{ _globPattern = Text.pack globPattern,
|
||||||
|
_kind = Nothing
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
sourceFilesChangedHandler
|
||||||
|
case watchSourceFilesToken of
|
||||||
|
Nothing -> logM "[initializedHandler] Client did not accept WorkspaceDidChangeWatchedFiles registration"
|
||||||
|
Just _ -> logM $ "[initializedHandler] WorkspaceDidChangeWatchedFiles registered for JS/TS source files. Glob pattern: " <> globPattern
|
||||||
|
modify (State.regTokens . State.watchSourceFilesToken .~ watchSourceFilesToken)
|
||||||
|
|
||||||
|
-- | Ran when files in src/ change. It refreshes the relevant export lists in
|
||||||
|
-- the cache and updates missing import diagnostics.
|
||||||
|
--
|
||||||
|
-- Both of these tasks are ran in the reactor thread so that other requests
|
||||||
|
-- can still be answered.
|
||||||
|
sourceFilesChangedHandler :: LSP.Handler ServerM 'LSP.WorkspaceDidChangeWatchedFiles
|
||||||
|
sourceFilesChangedHandler msg = do
|
||||||
|
let (LSP.List uris) = fmap (^. LSP.uri) $ msg ^. LSP.params . LSP.changes
|
||||||
|
logM $ "[watchSourceFilesHandler] Received file changes: " ++ show uris
|
||||||
|
let fileUris = mapMaybe (SP.parseAbsFile <=< stripPrefix "file://" . T.unpack . LSP.getUri) uris
|
||||||
|
forM_ fileUris $ \file -> sendToReactor $ do
|
||||||
|
-- Refresh export list for modified file
|
||||||
|
refreshExportsOfFiles [file]
|
||||||
|
-- Update diagnostics for the wasp file
|
||||||
|
updateMissingExtImportDiagnostics
|
||||||
|
handler $
|
||||||
|
asks (^. State.waspFileUri) >>= \case
|
||||||
|
Just uri -> do
|
||||||
|
logM $ "[watchSourceFilesHandler] Updating missing diagnostics for " ++ show uri
|
||||||
|
publishDiagnostics uri
|
||||||
|
Nothing -> pure ()
|
@ -1,301 +0,0 @@
|
|||||||
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
|
|
||||||
|
|
||||||
{-# HLINT ignore "Redundant <$>" #-}
|
|
||||||
|
|
||||||
module Wasp.LSP.ExtImport
|
|
||||||
( -- * TS Export lists
|
|
||||||
refreshAllExports,
|
|
||||||
refreshExportsForFiles,
|
|
||||||
|
|
||||||
-- * Diagnostics and Syntax
|
|
||||||
ExtImportNode (..),
|
|
||||||
findExtImportAroundLocation,
|
|
||||||
ExtImportLookupResult (..),
|
|
||||||
lookupExtImport,
|
|
||||||
updateMissingImportDiagnostics,
|
|
||||||
getMissingImportDiagnostics,
|
|
||||||
)
|
|
||||||
where
|
|
||||||
|
|
||||||
import Control.Applicative ((<|>))
|
|
||||||
import Control.Arrow (Arrow (first), (&&&))
|
|
||||||
import Control.Lens ((%~), (^.))
|
|
||||||
import Control.Monad (unless, void)
|
|
||||||
import Control.Monad.IO.Class (MonadIO, liftIO)
|
|
||||||
import Control.Monad.Log.Class (logM)
|
|
||||||
import Control.Monad.Reader.Class (asks)
|
|
||||||
import Control.Monad.Trans.Maybe (MaybeT (runMaybeT))
|
|
||||||
import qualified Data.HashMap.Strict as M
|
|
||||||
import Data.List (find, stripPrefix)
|
|
||||||
import Data.Maybe (catMaybes, fromJust, isNothing, mapMaybe)
|
|
||||||
import qualified Language.LSP.Server as LSP
|
|
||||||
import qualified Path as P
|
|
||||||
import qualified StrongPath as SP
|
|
||||||
import qualified StrongPath.Path as SP
|
|
||||||
import Text.Read (readMaybe)
|
|
||||||
import Wasp.Analyzer.Parser (ExtImportName (ExtImportField, ExtImportModule))
|
|
||||||
import qualified Wasp.Analyzer.Parser.CST as S
|
|
||||||
import Wasp.Analyzer.Parser.CST.Traverse (Traversal)
|
|
||||||
import qualified Wasp.Analyzer.Parser.CST.Traverse as T
|
|
||||||
import Wasp.LSP.Diagnostic (MissingImportReason (NoDefaultExport, NoFile, NoNamedExport), WaspDiagnostic (MissingImportDiagnostic), clearMissingImportDiagnostics)
|
|
||||||
import Wasp.LSP.ServerM (HandlerM, ServerM, handler, modify)
|
|
||||||
import qualified Wasp.LSP.ServerState as State
|
|
||||||
import Wasp.LSP.Syntax (findChild, lexemeAt)
|
|
||||||
import Wasp.LSP.Util (hoistMaybe)
|
|
||||||
import Wasp.Project (WaspProjectDir)
|
|
||||||
import qualified Wasp.TypeScript as TS
|
|
||||||
import Wasp.Util.IO (doesFileExist)
|
|
||||||
|
|
||||||
-- | Finds all external imports and refreshes the export cache for the relevant
|
|
||||||
-- files.
|
|
||||||
refreshAllExports :: ServerM ()
|
|
||||||
refreshAllExports = do
|
|
||||||
(src, maybeCst) <- handler $ asks ((^. State.currentWaspSource) &&& (^. State.cst))
|
|
||||||
maybeWaspRoot <- (>>= SP.parseAbsDir) <$> LSP.getRootPath
|
|
||||||
case (,) <$> maybeCst <*> maybeWaspRoot of
|
|
||||||
Nothing -> pure ()
|
|
||||||
Just (syntax, waspRoot) -> do
|
|
||||||
let allExtImports = findAllExtImports src syntax
|
|
||||||
allTsFiles <- catMaybes <$> mapM (absPathForExtImport waspRoot) allExtImports
|
|
||||||
refreshExportsForFiles allTsFiles
|
|
||||||
|
|
||||||
-- | Refresh the export cache for the given JS/TS files. This can take a while:
|
|
||||||
-- generally half a second to a second. It is recommended that this is run in
|
|
||||||
-- the reactor thread so it does not block other LSP requests from being
|
|
||||||
-- responded to.
|
|
||||||
refreshExportsForFiles :: [SP.Path' SP.Abs SP.File'] -> ServerM ()
|
|
||||||
refreshExportsForFiles files = do
|
|
||||||
logM $ "[refreshExportsForFile] refreshing export lists for " ++ show files
|
|
||||||
|
|
||||||
-- First, remove any deleted files from the cache
|
|
||||||
mapM_ clearCacheForFileIfMissing files
|
|
||||||
|
|
||||||
LSP.getRootPath >>= \case
|
|
||||||
Nothing -> pure ()
|
|
||||||
Just projectDirFilepath -> do
|
|
||||||
-- NOTE: getRootPath always returns a valid absolute path or 'Nothing'.
|
|
||||||
let projectDir = fromJust $ SP.parseAbsDir projectDirFilepath
|
|
||||||
let exportRequests = mapMaybe (getExportRequestForFile projectDir) files
|
|
||||||
liftIO (TS.getExportsOfTsFiles exportRequests) >>= \case
|
|
||||||
Left err -> do
|
|
||||||
logM $ "[refreshExportsForFile] ERROR getting exports: " ++ show err
|
|
||||||
Right res -> updateExportsCache res
|
|
||||||
where
|
|
||||||
getExportRequestForFile projectDir file =
|
|
||||||
([SP.fromAbsFile file] `TS.TsExportRequest`) . Just . SP.fromAbsFile <$> tryGetTsconfigForFile projectDir file
|
|
||||||
|
|
||||||
-- Removes deleted files from cache
|
|
||||||
clearCacheForFileIfMissing file = do
|
|
||||||
fileExists <- liftIO $ doesFileExist file
|
|
||||||
unless fileExists $ modify (State.tsExports %~ M.insert file [])
|
|
||||||
|
|
||||||
-- | Look for the tsconfig file for the specified JS/TS file.
|
|
||||||
--
|
|
||||||
-- To do this, it checks if the file is inside src/client or src/server and
|
|
||||||
-- returns the respective tsconfig path if so (src/client/tsconfig.json or
|
|
||||||
-- src/server/tsconfig.json).
|
|
||||||
tryGetTsconfigForFile :: SP.Path' SP.Abs (SP.Dir WaspProjectDir) -> SP.Path' SP.Abs SP.File' -> Maybe (SP.Path' SP.Abs SP.File')
|
|
||||||
tryGetTsconfigForFile waspRoot file = tsconfigPath [SP.reldir|src/client|] <|> tsconfigPath [SP.reldir|src/server|]
|
|
||||||
where
|
|
||||||
tsconfigPath :: SP.Path' (SP.Rel WaspProjectDir) SP.Dir' -> Maybe (SP.Path' SP.Abs SP.File')
|
|
||||||
tsconfigPath folder =
|
|
||||||
let absFolder = waspRoot SP.</> folder
|
|
||||||
in if SP.toPathAbsDir absFolder `P.isProperPrefixOf` SP.toPathAbsFile file
|
|
||||||
then Just $ absFolder SP.</> [SP.relfile|tsconfig.json|]
|
|
||||||
else Nothing
|
|
||||||
|
|
||||||
updateExportsCache :: TS.TsExportResponse -> ServerM ()
|
|
||||||
updateExportsCache (TS.TsExportResponse res) = do
|
|
||||||
let newExports = M.fromList $ map (first exportResKeyToPath) $ M.toList res
|
|
||||||
void $ modify $ State.tsExports %~ (newExports `M.union`)
|
|
||||||
where
|
|
||||||
-- 'TS.getExportsOfTsFiles' should only ever put valid paths in the keys of
|
|
||||||
-- its response, so we enforce that here.
|
|
||||||
exportResKeyToPath key = case SP.parseAbsFile key of
|
|
||||||
Just path -> path
|
|
||||||
Nothing -> error "updateExportsCache: expected valid path from TS.getExportsOfTsFiles."
|
|
||||||
|
|
||||||
-- ------------------------- Diagnostics & Syntax ------------------------------
|
|
||||||
|
|
||||||
data ExtImportNode = ExtImportNode
|
|
||||||
{ -- | Location of the 'S.ExtImport' node.
|
|
||||||
einLocation :: !Traversal,
|
|
||||||
einName :: !(Maybe ExtImportName),
|
|
||||||
-- | Imported filepath, verbatim from the wasp source file.
|
|
||||||
einFile :: !(Maybe FilePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
-- | Create a 'ExtImportNode' at a location, assuming that the given node is
|
|
||||||
-- a 'S.ExtImport'.
|
|
||||||
extImportAtLocation :: String -> Traversal -> ExtImportNode
|
|
||||||
extImportAtLocation src location =
|
|
||||||
let maybeName =
|
|
||||||
(ExtImportModule . lexemeAt src <$> findChild S.ExtImportModule location)
|
|
||||||
<|> (ExtImportField . lexemeAt src <$> findChild S.ExtImportField location)
|
|
||||||
maybeFile = lexemeAt src <$> findChild S.ExtImportPath location
|
|
||||||
in ExtImportNode location maybeName maybeFile
|
|
||||||
|
|
||||||
-- | Search for an 'S.ExtImport' node at the current node or as one of its
|
|
||||||
-- ancestors.
|
|
||||||
findExtImportAroundLocation ::
|
|
||||||
-- | Wasp source code.
|
|
||||||
String ->
|
|
||||||
-- | Location to look for external import at.
|
|
||||||
Traversal ->
|
|
||||||
Maybe ExtImportNode
|
|
||||||
findExtImportAroundLocation src location = do
|
|
||||||
extImport <- findExtImportParent location
|
|
||||||
return $ extImportAtLocation src extImport
|
|
||||||
where
|
|
||||||
findExtImportParent t
|
|
||||||
| T.kindAt t == S.ExtImport = Just t
|
|
||||||
| otherwise = T.up t >>= findExtImportParent
|
|
||||||
|
|
||||||
-- | Gets diagnostics for external imports and appends them to the current
|
|
||||||
-- list of diagnostics.
|
|
||||||
updateMissingImportDiagnostics :: ServerM ()
|
|
||||||
updateMissingImportDiagnostics = do
|
|
||||||
newDiagnostics <- handler getMissingImportDiagnostics
|
|
||||||
modify (State.latestDiagnostics %~ ((++ newDiagnostics) . clearMissingImportDiagnostics))
|
|
||||||
|
|
||||||
-- | Get diagnostics for external imports with missing definitions. Uses the
|
|
||||||
-- cached export lists.
|
|
||||||
getMissingImportDiagnostics :: HandlerM [WaspDiagnostic]
|
|
||||||
getMissingImportDiagnostics =
|
|
||||||
asks (^. State.cst) >>= \case
|
|
||||||
Nothing -> return []
|
|
||||||
Just syntax -> do
|
|
||||||
src <- asks (^. State.currentWaspSource)
|
|
||||||
let allExtImports = findAllExtImports src syntax
|
|
||||||
catMaybes <$> mapM findDiagnosticForExtImport allExtImports
|
|
||||||
|
|
||||||
-- Finds all external imports in a concrete syntax tree.
|
|
||||||
findAllExtImports :: String -> [S.SyntaxNode] -> [ExtImportNode]
|
|
||||||
findAllExtImports src syntax = go $ T.fromSyntaxForest syntax
|
|
||||||
where
|
|
||||||
-- Recurse through syntax tree and find all 'S.ExtImport' nodes.
|
|
||||||
go :: Traversal -> [ExtImportNode]
|
|
||||||
go t = case T.kindAt t of
|
|
||||||
S.ExtImport -> [extImportAtLocation src t]
|
|
||||||
_ -> concatMap go $ T.children t
|
|
||||||
|
|
||||||
-- | The result of 'lookupExtImport'.
|
|
||||||
data ExtImportLookupResult
|
|
||||||
= -- | There is a syntax error in the ExtImport.
|
|
||||||
ImportSyntaxError
|
|
||||||
| -- | The imported file exists but is not in cached export list.
|
|
||||||
ImportCacheMiss
|
|
||||||
| -- | The imported file does not exist.
|
|
||||||
ImportedFileDoesNotExist (SP.Path' SP.Abs SP.File')
|
|
||||||
| -- | Imports a symbol that is not exported from the file it imports.
|
|
||||||
ImportedSymbolDoesNotExist (SP.Path' SP.Abs SP.File')
|
|
||||||
| -- | Sucessful lookup: includes the file and exported symbol.
|
|
||||||
ImportsSymbol (SP.Path' SP.Abs SP.File') TS.TsExport
|
|
||||||
deriving (Eq, Show)
|
|
||||||
|
|
||||||
-- | Search the cached export list for the export that the 'ExtImportNode'
|
|
||||||
-- imports, if any exists.
|
|
||||||
lookupExtImport :: ExtImportNode -> HandlerM ExtImportLookupResult
|
|
||||||
lookupExtImport extImport = do
|
|
||||||
maybeWaspRoot <- (>>= SP.parseAbsDir) <$> LSP.getRootPath
|
|
||||||
case maybeWaspRoot of
|
|
||||||
Nothing -> return ImportSyntaxError
|
|
||||||
Just waspRoot -> do
|
|
||||||
absPathForExtImport waspRoot extImport >>= \case
|
|
||||||
Nothing -> do
|
|
||||||
return ImportSyntaxError
|
|
||||||
Just tsFile ->
|
|
||||||
asks ((M.!? tsFile) . (^. State.tsExports)) >>= \case
|
|
||||||
Nothing -> lookupCacheMiss tsFile
|
|
||||||
Just exports -> lookupCacheHit tsFile exports
|
|
||||||
where
|
|
||||||
lookupCacheMiss tsFile = do
|
|
||||||
tsFileExists <- liftIO $ doesFileExist tsFile
|
|
||||||
if tsFileExists
|
|
||||||
then return ImportCacheMiss
|
|
||||||
else return $ ImportedFileDoesNotExist tsFile
|
|
||||||
|
|
||||||
lookupCacheHit tsFile exports = case maybeIsImportedExport of
|
|
||||||
Nothing -> return ImportSyntaxError
|
|
||||||
Just isImportedExport -> do
|
|
||||||
case find isImportedExport exports of
|
|
||||||
Just export -> return $ ImportsSymbol tsFile export
|
|
||||||
Nothing -> return $ ImportedSymbolDoesNotExist tsFile
|
|
||||||
|
|
||||||
-- A predicate to check if a TsExport matches the ExtImport, assuming the
|
|
||||||
-- export is from the correct file.
|
|
||||||
maybeIsImportedExport = case einName extImport of
|
|
||||||
Nothing -> Nothing
|
|
||||||
Just (ExtImportModule _) -> Just $ \case
|
|
||||||
TS.DefaultExport _ -> True
|
|
||||||
_ -> False
|
|
||||||
Just (ExtImportField name) -> Just $ \case
|
|
||||||
TS.NamedExport n _ | n == name -> True
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
-- | Check a single external import and see if it points to a real exported
|
|
||||||
-- function in a source file.
|
|
||||||
--
|
|
||||||
-- If the file is not in the cache, no diagnostic is reported because that would
|
|
||||||
-- risk showing incorrect diagnostics.
|
|
||||||
findDiagnosticForExtImport :: ExtImportNode -> HandlerM (Maybe WaspDiagnostic)
|
|
||||||
findDiagnosticForExtImport extImport =
|
|
||||||
lookupExtImport extImport >>= \case
|
|
||||||
ImportSyntaxError -> do
|
|
||||||
logM $ "[getMissingImportDiagnostics] ignoring extimport with a syntax error " ++ show extImportSpan
|
|
||||||
return Nothing
|
|
||||||
ImportCacheMiss -> return Nothing
|
|
||||||
ImportedFileDoesNotExist tsFile -> return $ Just $ MissingImportDiagnostic extImportSpan NoFile tsFile
|
|
||||||
ImportedSymbolDoesNotExist tsFile -> return $ Just $ diagnosticForExtImport tsFile
|
|
||||||
ImportsSymbol _ _ -> return Nothing -- Valid extimport, no diagnostic to report.
|
|
||||||
where
|
|
||||||
diagnosticForExtImport tsFile = case einName extImport of
|
|
||||||
Nothing -> error "diagnosticForExtImport called for nameless ext import. This should never happen."
|
|
||||||
Just (ExtImportModule _) -> MissingImportDiagnostic extImportSpan NoDefaultExport tsFile
|
|
||||||
Just (ExtImportField name) -> MissingImportDiagnostic extImportSpan (NoNamedExport name) tsFile
|
|
||||||
|
|
||||||
extImportSpan = T.spanAt $ einLocation extImport
|
|
||||||
|
|
||||||
-- | Convert the path inside an external import in a .wasp file to an absolute
|
|
||||||
-- path.
|
|
||||||
--
|
|
||||||
-- To support module resolution, this first tries to find the file with the
|
|
||||||
-- exact extension, otherwise it tries to replace @.js@ with @.ts@ or it tries
|
|
||||||
-- to append @.js@, @.jsx@, @.ts@, @.tsx@ if the file has no extension.
|
|
||||||
absPathForExtImport ::
|
|
||||||
(MonadIO m) =>
|
|
||||||
SP.Path' SP.Abs SP.Dir' ->
|
|
||||||
ExtImportNode ->
|
|
||||||
m (Maybe (SP.Path' SP.Abs SP.File'))
|
|
||||||
absPathForExtImport waspRoot extImport = runMaybeT $ do
|
|
||||||
-- Read the string from the syntax tree
|
|
||||||
extImportPath :: FilePath <- hoistMaybe $ einFile extImport >>= readMaybe
|
|
||||||
-- Drop the @ and try to parse to a relative path
|
|
||||||
relPath <- hoistMaybe $ SP.parseRelFile =<< stripPrefix "@" extImportPath
|
|
||||||
-- Prepend the src directory in the project to the relative path
|
|
||||||
let absPath = waspRoot SP.</> [SP.reldir|src|] SP.</> relPath
|
|
||||||
-- Fix the extension, if needed
|
|
||||||
SP.fromPathAbsFile <$> fixExtension (SP.toPathAbsFile absPath)
|
|
||||||
where
|
|
||||||
fixExtension file
|
|
||||||
| isNothing ext = useExtensionsIfExists [".jsx", ".tsx", ".js", ".ts"] file
|
|
||||||
| ext == Just ".js" = useExtensionsIfExists [".ts"] file
|
|
||||||
| otherwise = return file
|
|
||||||
where
|
|
||||||
ext = P.fileExtension file
|
|
||||||
|
|
||||||
-- Returns @Nothing@ if @file@ does not exist, otherwise returns @Just file@.
|
|
||||||
ifExists file = do
|
|
||||||
exists <- liftIO $ doesFileExist $ SP.fromPathAbsFile file
|
|
||||||
if exists
|
|
||||||
then return $ Just file
|
|
||||||
else return Nothing
|
|
||||||
|
|
||||||
-- Replaces the extension of @file@ with the left-most extension such that
|
|
||||||
-- the new file path exists. If no such extension is given, returns the
|
|
||||||
-- original file path.
|
|
||||||
useExtensionsIfExists [] file = return file
|
|
||||||
useExtensionsIfExists (ext : exts) file =
|
|
||||||
ifExists (fromJust $ P.replaceExtension ext file) >>= \case
|
|
||||||
Nothing -> useExtensionsIfExists exts file
|
|
||||||
Just file' -> return file'
|
|
51
waspc/waspls/src/Wasp/LSP/ExtImport/Diagnostic.hs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
module Wasp.LSP.ExtImport.Diagnostic
|
||||||
|
( updateMissingExtImportDiagnostics,
|
||||||
|
getMissingExtImportDiagnostics,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Lens ((%~), (^.))
|
||||||
|
import Control.Monad.Reader.Class (asks)
|
||||||
|
import Data.Maybe (catMaybes)
|
||||||
|
import Wasp.Analyzer.Parser (ExtImportName (ExtImportField, ExtImportModule))
|
||||||
|
import qualified Wasp.Analyzer.Parser.CST.Traverse as T
|
||||||
|
import Wasp.LSP.Diagnostic (MissingExtImportReason (..), WaspDiagnostic (MissingExtImportDiagnostic), clearMissingExtImportDiagnostics)
|
||||||
|
import Wasp.LSP.ExtImport.ExportsCache (ExtImportLookupResult (..), lookupExtImport)
|
||||||
|
import Wasp.LSP.ExtImport.Syntax (ExtImportNode (einLocation), getAllExtImports)
|
||||||
|
import Wasp.LSP.ServerMonads (HandlerM, ServerM, handler, modify)
|
||||||
|
import qualified Wasp.LSP.ServerState as State
|
||||||
|
|
||||||
|
-- | Clears missing external import diagnostics and computes new diagnostics to
|
||||||
|
-- replace them with. Does not publish the diagnostics.
|
||||||
|
updateMissingExtImportDiagnostics :: ServerM ()
|
||||||
|
updateMissingExtImportDiagnostics = do
|
||||||
|
newDiagnostics <- handler getMissingExtImportDiagnostics
|
||||||
|
modify (State.latestDiagnostics %~ ((++ newDiagnostics) . clearMissingExtImportDiagnostics))
|
||||||
|
|
||||||
|
-- | Finds missing external import diagnostics for all ExtImports in the current
|
||||||
|
-- concrete syntax tree.
|
||||||
|
getMissingExtImportDiagnostics :: HandlerM [WaspDiagnostic]
|
||||||
|
getMissingExtImportDiagnostics =
|
||||||
|
asks (^. State.cst) >>= \case
|
||||||
|
Nothing -> return []
|
||||||
|
Just syntax -> do
|
||||||
|
src <- asks (^. State.currentWaspSource)
|
||||||
|
let allExtImports = getAllExtImports src syntax
|
||||||
|
catMaybes <$> mapM findDiagnosticForExtImport allExtImports
|
||||||
|
|
||||||
|
-- | Check for a diagnostic at a single external import.
|
||||||
|
findDiagnosticForExtImport :: ExtImportNode -> HandlerM (Maybe WaspDiagnostic)
|
||||||
|
findDiagnosticForExtImport extImport =
|
||||||
|
lookupExtImport extImport >>= \case
|
||||||
|
ImportSyntaxError -> return Nothing -- Syntax errors are already reported elsewhere.
|
||||||
|
ImportCacheMiss -> return Nothing
|
||||||
|
ImportedFileDoesNotExist file -> return $ Just $ MissingExtImportDiagnostic extImportSpan NoFile file
|
||||||
|
ImportedSymbolDoesNotExist symbol file -> return $ Just $ diagnosticForExtImport symbol file
|
||||||
|
ImportsSymbol _ _ -> return Nothing -- Valid import.
|
||||||
|
where
|
||||||
|
diagnosticForExtImport (ExtImportModule _) file =
|
||||||
|
MissingExtImportDiagnostic extImportSpan NoDefaultExport file
|
||||||
|
diagnosticForExtImport (ExtImportField name) file =
|
||||||
|
MissingExtImportDiagnostic extImportSpan (NoNamedExport name) file
|
||||||
|
|
||||||
|
extImportSpan = T.spanAt $ einLocation extImport
|
133
waspc/waspls/src/Wasp/LSP/ExtImport/ExportsCache.hs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
module Wasp.LSP.ExtImport.ExportsCache
|
||||||
|
( refreshExportsForAllExtImports,
|
||||||
|
refreshExportsOfFiles,
|
||||||
|
lookupExtImport,
|
||||||
|
ExtImportLookupResult (..),
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Arrow ((&&&))
|
||||||
|
import Control.Lens ((%~), (^.))
|
||||||
|
import Control.Monad (void, (<=<))
|
||||||
|
import Control.Monad.IO.Class (liftIO)
|
||||||
|
import Control.Monad.Log.Class (logM)
|
||||||
|
import Control.Monad.Reader.Class (asks)
|
||||||
|
import qualified Data.HashMap.Strict as M
|
||||||
|
import Data.List (find)
|
||||||
|
import Data.Maybe (catMaybes, fromJust, mapMaybe)
|
||||||
|
import qualified Language.LSP.Server as LSP
|
||||||
|
import qualified StrongPath as SP
|
||||||
|
import Wasp.Analyzer.Parser (ExtImportName (ExtImportField, ExtImportModule))
|
||||||
|
import Wasp.LSP.ExtImport.Path (WaspStyleExtFilePath, absPathToCachePath, cachePathToAbsPath, tryGetTsconfigForAbsPath, waspStylePathToCachePath)
|
||||||
|
import Wasp.LSP.ExtImport.Syntax (ExtImportNode (einName, einPath), getAllExtImports)
|
||||||
|
import Wasp.LSP.ServerMonads (HandlerM, ServerM, handler, modify)
|
||||||
|
import qualified Wasp.LSP.ServerState as State
|
||||||
|
import qualified Wasp.TypeScript.Inspect.Exports as TS
|
||||||
|
|
||||||
|
-- | Based on the files imported in the external imports of the current concrete
|
||||||
|
-- syntax tree, refreshes the exports of files that are imported.
|
||||||
|
refreshExportsForAllExtImports :: ServerM ()
|
||||||
|
refreshExportsForAllExtImports = do
|
||||||
|
(src, maybeCst) <- handler $ asks ((^. State.currentWaspSource) &&& (^. State.cst))
|
||||||
|
case maybeCst of
|
||||||
|
Nothing -> pure ()
|
||||||
|
Just syntax -> do
|
||||||
|
let allExtImports = getAllExtImports src syntax
|
||||||
|
let allCachePaths = mapMaybe (waspStylePathToCachePath <=< einPath) allExtImports
|
||||||
|
allTsFiles <- catMaybes <$> mapM cachePathToAbsPath allCachePaths
|
||||||
|
refreshExportsOfFiles allTsFiles
|
||||||
|
|
||||||
|
-- | Update the exports list of several files. Clears the previous cache for
|
||||||
|
-- each of the files listed, even if it fails to get a new list of exports.
|
||||||
|
refreshExportsOfFiles :: [SP.Path' SP.Abs SP.File'] -> ServerM ()
|
||||||
|
refreshExportsOfFiles files = do
|
||||||
|
logM $ "[refreshExportsOfFiles] refreshing export lists for " ++ show files
|
||||||
|
|
||||||
|
cachePaths <- catMaybes <$> mapM absPathToCachePath files
|
||||||
|
|
||||||
|
LSP.getRootPath >>= \case
|
||||||
|
Nothing -> pure ()
|
||||||
|
Just projectRootDirFilePath -> do
|
||||||
|
let projectRootDir = fromJust $ SP.parseAbsDir projectRootDirFilePath
|
||||||
|
let exportRequests = mapMaybe (getExportRequestForFile projectRootDir) files
|
||||||
|
response <- liftIO (TS.getExportsOfTsFiles exportRequests)
|
||||||
|
-- Clear cache for all the files that were requested to be updated. This
|
||||||
|
-- takes care of removing deleted files from the cache.
|
||||||
|
modify (State.tsExports %~ foldr ((.) . M.delete) id cachePaths)
|
||||||
|
case response of
|
||||||
|
Left err -> do
|
||||||
|
logM $ "[refreshExportsForFile] ERROR getting exports: " ++ show err
|
||||||
|
Right res -> do
|
||||||
|
updateExportsCache res
|
||||||
|
where
|
||||||
|
-- Find the tsconfig for a given file and return an export request including
|
||||||
|
-- that tsconfig. If a tsconfig can not be found (see 'tryGetTsConfigForAbsPath'
|
||||||
|
-- for why that might happen), no export request is no returned.
|
||||||
|
getExportRequestForFile projectRootDir file = do
|
||||||
|
tsconfigPath <- tryGetTsconfigForAbsPath projectRootDir file
|
||||||
|
return $
|
||||||
|
TS.TsExportsRequest
|
||||||
|
{ TS.filepaths = [SP.fromAbsFile file],
|
||||||
|
TS.tsconfig = Just $ SP.fromAbsFile tsconfigPath
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Replaces entries in the exports cache with the exports lists in the
|
||||||
|
-- response.
|
||||||
|
updateExportsCache :: TS.TsExportsResponse -> ServerM ()
|
||||||
|
updateExportsCache (TS.TsExportsResponse res) = do
|
||||||
|
newExports <- M.fromList <$> mapM exportResKeyToCachePath (M.toList res)
|
||||||
|
void $ modify $ State.tsExports %~ (newExports `M.union`)
|
||||||
|
|
||||||
|
exportResKeyToCachePath (key, exports) = case SP.parseAbsFile key of
|
||||||
|
Nothing -> error "[refreshExportsOfFiles]: TS.getExportsOfTsFiles did not respond with a valid path"
|
||||||
|
Just path ->
|
||||||
|
absPathToCachePath path >>= \case
|
||||||
|
Nothing -> error "[refreshExportsOfFiles]: exports refreshed outside of wasp src/ dir (could not get cache path)"
|
||||||
|
Just cachePath -> return (cachePath, exports)
|
||||||
|
|
||||||
|
-- | The result of 'lookupExtImport'.
|
||||||
|
data ExtImportLookupResult
|
||||||
|
= -- | There is a syntax error in the ExtImport.
|
||||||
|
ImportSyntaxError
|
||||||
|
| -- | The imported file exists but is not in cached export list.
|
||||||
|
ImportCacheMiss
|
||||||
|
| -- | The imported file does not exist.
|
||||||
|
ImportedFileDoesNotExist !WaspStyleExtFilePath
|
||||||
|
| -- | Imports a symbol that is not exported from the file it imports.
|
||||||
|
ImportedSymbolDoesNotExist !ExtImportName !WaspStyleExtFilePath
|
||||||
|
| -- | Sucessful lookup: includes the file and exported symbol.
|
||||||
|
ImportsSymbol !(SP.Path' SP.Abs SP.File') TS.TsExport
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- | Find the 'TS.TsExport' for the given external import in the export cache.
|
||||||
|
lookupExtImport :: ExtImportNode -> HandlerM ExtImportLookupResult
|
||||||
|
lookupExtImport extImport = case (waspStylePathToCachePath =<< einPath extImport, einPath extImport) of
|
||||||
|
(Just cachePath, Just waspStylePath) ->
|
||||||
|
asks ((M.!? cachePath) . (^. State.tsExports)) >>= \case
|
||||||
|
Nothing -> lookupCacheMiss waspStylePath cachePath
|
||||||
|
Just exports -> lookupCacheHit cachePath waspStylePath exports
|
||||||
|
_ -> return ImportSyntaxError -- No import path provided or the provided path is invalid.
|
||||||
|
where
|
||||||
|
lookupCacheMiss waspStylePath cachePath =
|
||||||
|
cachePathToAbsPath cachePath >>= \case
|
||||||
|
Nothing -> return $ ImportedFileDoesNotExist waspStylePath
|
||||||
|
Just _ -> return ImportCacheMiss
|
||||||
|
|
||||||
|
lookupCacheHit cachePath waspStylePath exports = case einName extImport of
|
||||||
|
Nothing -> return ImportSyntaxError -- No valid name imported.
|
||||||
|
Just name -> do
|
||||||
|
case find (isImportedExport name) exports of
|
||||||
|
Just export ->
|
||||||
|
cachePathToAbsPath cachePath >>= \case
|
||||||
|
Nothing -> error "[lookupExtImport]: file does not exist after verifying the import is valid"
|
||||||
|
Just tsFile -> return $ ImportsSymbol tsFile export
|
||||||
|
Nothing -> return $ ImportedSymbolDoesNotExist name waspStylePath
|
||||||
|
|
||||||
|
-- A predicate to check if a 'TS.TsExport' matches an imported 'ExtImportName'.
|
||||||
|
isImportedExport importedName = case importedName of
|
||||||
|
ExtImportModule _ -> \case
|
||||||
|
TS.DefaultExport _ -> True
|
||||||
|
_ -> False
|
||||||
|
ExtImportField name -> \case
|
||||||
|
TS.NamedExport n _ | n == name -> True
|
||||||
|
_ -> False
|
159
waspc/waspls/src/Wasp/LSP/ExtImport/Path.hs
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
|
||||||
|
module Wasp.LSP.ExtImport.Path
|
||||||
|
( ExtFileCachePath,
|
||||||
|
WaspStyleExtFilePath (WaspStyleExtFilePath),
|
||||||
|
waspStylePathToCachePath,
|
||||||
|
absPathToCachePath,
|
||||||
|
cachePathToAbsPath,
|
||||||
|
tryGetTsconfigForAbsPath,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Applicative ((<|>))
|
||||||
|
import Control.Monad.IO.Class (MonadIO (liftIO))
|
||||||
|
import Data.Hashable (Hashable (hashWithSalt))
|
||||||
|
import Data.List (stripPrefix)
|
||||||
|
import GHC.Generics (Generic)
|
||||||
|
import qualified Language.LSP.Server as LSP
|
||||||
|
import qualified Path as P
|
||||||
|
import qualified StrongPath as SP
|
||||||
|
import qualified StrongPath.Path as SP
|
||||||
|
import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir)
|
||||||
|
import Wasp.Project.Common (WaspProjectDir)
|
||||||
|
import Wasp.Util.IO (doesFileExist)
|
||||||
|
|
||||||
|
data ExtensionlessExtFile
|
||||||
|
|
||||||
|
-- | Path used in the exports cache for external code files (JS and TS files).
|
||||||
|
--
|
||||||
|
-- It is stored relative to @src/@ and without an extension so that cache lookups
|
||||||
|
-- (starting with a 'WaspStyleExtFilePath') are more efficient.
|
||||||
|
data ExtFileCachePath
|
||||||
|
= ExtFileCachePath !(SP.Path' (SP.Rel SourceExternalCodeDir) (SP.File ExtensionlessExtFile)) !ExtensionType
|
||||||
|
deriving (Show, Eq, Generic)
|
||||||
|
|
||||||
|
-- | Hashes only the path portion (ignoring the extension type). This is so that
|
||||||
|
-- hashing does not get in the way of equality comparisons while looking up
|
||||||
|
-- 'ExtFileCachePath's in a "Data.HashMap".
|
||||||
|
instance Hashable ExtFileCachePath where
|
||||||
|
hashWithSalt salt (ExtFileCachePath cachePath _) = hashWithSalt salt cachePath
|
||||||
|
|
||||||
|
-- | A path that would appear in an external import, exactly as it is written in
|
||||||
|
-- Wasp source code.
|
||||||
|
--
|
||||||
|
-- For example, @\"\@server/queries.js\"@ is a wasp style path, but @\"src\/server\/queries.js\"@
|
||||||
|
-- is not.
|
||||||
|
newtype WaspStyleExtFilePath = WaspStyleExtFilePath String deriving (Show, Eq)
|
||||||
|
|
||||||
|
waspStylePathToCachePath :: WaspStyleExtFilePath -> Maybe ExtFileCachePath
|
||||||
|
waspStylePathToCachePath (WaspStyleExtFilePath waspStylePath) = do
|
||||||
|
-- Removes the @, making it a path relative to src/, and remove the extension.
|
||||||
|
relFile <- P.parseRelFile =<< stripPrefix "@" waspStylePath
|
||||||
|
let (extensionLessFile, extType) = splitExtensionType relFile
|
||||||
|
return $ ExtFileCachePath (SP.fromPathRelFile extensionLessFile) extType
|
||||||
|
|
||||||
|
absPathToCachePath :: LSP.MonadLsp c m => SP.Path' SP.Abs SP.File' -> m (Maybe ExtFileCachePath)
|
||||||
|
absPathToCachePath absFile = do
|
||||||
|
-- Makes the path relative to src/ and deletes the extension.
|
||||||
|
maybeProjectDir <- (>>= SP.parseAbsDir) <$> LSP.getRootPath
|
||||||
|
case maybeProjectDir of
|
||||||
|
Nothing -> pure Nothing
|
||||||
|
Just (projectRootDir :: SP.Path' SP.Abs (SP.Dir WaspProjectDir)) ->
|
||||||
|
let srcDir = projectRootDir SP.</> srcDirInProjectRootDir
|
||||||
|
in case P.stripProperPrefix (SP.toPathAbsDir srcDir) (SP.toPathAbsFile absFile) of
|
||||||
|
Nothing -> pure Nothing
|
||||||
|
Just relFile -> do
|
||||||
|
let (extensionLessFile, extType) = splitExtensionType relFile
|
||||||
|
pure $ Just $ ExtFileCachePath (SP.fromPathRelFile extensionLessFile) extType
|
||||||
|
|
||||||
|
cachePathToAbsPath :: forall m c. LSP.MonadLsp c m => ExtFileCachePath -> m (Maybe (SP.Path' SP.Abs SP.File'))
|
||||||
|
cachePathToAbsPath (ExtFileCachePath cachePath extType) = do
|
||||||
|
-- Converts to an absolute path and finds the appropriate extension.
|
||||||
|
maybeProjectDir <- (>>= SP.parseAbsDir) <$> LSP.getRootPath
|
||||||
|
case maybeProjectDir of
|
||||||
|
Nothing -> pure Nothing
|
||||||
|
Just (projectRootDir :: SP.Path' SP.Abs (SP.Dir WaspProjectDir)) -> do
|
||||||
|
let fileWithNoExtension = projectRootDir SP.</> srcDirInProjectRootDir SP.</> cachePath
|
||||||
|
useFirstExtensionThatExists fileWithNoExtension $ allowedExts extType
|
||||||
|
where
|
||||||
|
useFirstExtensionThatExists :: SP.Path' SP.Abs (SP.File ExtensionlessExtFile) -> [String] -> m (Maybe (SP.Path' SP.Abs SP.File'))
|
||||||
|
useFirstExtensionThatExists _ [] = pure Nothing
|
||||||
|
useFirstExtensionThatExists file (ext : exts) =
|
||||||
|
case P.addExtension ext (SP.toPathAbsFile file) of
|
||||||
|
Nothing -> error "Invalid extension in return from 'allowedExts'"
|
||||||
|
Just fileWithExt -> do
|
||||||
|
fileWithExtExists <- liftIO $ doesFileExist $ SP.fromPathAbsFile fileWithExt
|
||||||
|
if fileWithExtExists
|
||||||
|
then pure $ Just $ SP.fromPathAbsFile fileWithExt
|
||||||
|
else useFirstExtensionThatExists file exts
|
||||||
|
|
||||||
|
-- | Try to find the @tsconfig.json@ file based on the location of the given
|
||||||
|
-- file.
|
||||||
|
--
|
||||||
|
-- Returns either @src/client/tsconfig.json@ or @src/server/tsconfig.json@,
|
||||||
|
-- depending on which directory the file is in. Does not check if those
|
||||||
|
-- config files exist.
|
||||||
|
--
|
||||||
|
-- IF the given path is not in either @src/@ subdirectory, returns nothing.
|
||||||
|
tryGetTsconfigForAbsPath :: SP.Path' SP.Abs (SP.Dir WaspProjectDir) -> SP.Path' SP.Abs SP.File' -> Maybe (SP.Path' SP.Abs SP.File')
|
||||||
|
tryGetTsconfigForAbsPath projectRootDir file = tsconfigPath [SP.reldir|src/client|] <|> tsconfigPath [SP.reldir|src/server|]
|
||||||
|
where
|
||||||
|
tsconfigPath :: SP.Path' (SP.Rel WaspProjectDir) SP.Dir' -> Maybe (SP.Path' SP.Abs SP.File')
|
||||||
|
tsconfigPath folder =
|
||||||
|
let absFolder = projectRootDir SP.</> folder
|
||||||
|
in if SP.toPathAbsDir absFolder `P.isProperPrefixOf` SP.toPathAbsFile file
|
||||||
|
then Just $ absFolder SP.</> [SP.relfile|tsconfig.json|]
|
||||||
|
else Nothing
|
||||||
|
|
||||||
|
srcDirInProjectRootDir :: SP.Path' (SP.Rel WaspProjectDir) (SP.Dir SourceExternalCodeDir)
|
||||||
|
srcDirInProjectRootDir = [SP.reldir|src|]
|
||||||
|
|
||||||
|
-- | The \"type\" of an extension. This type is related to how TypeScript resolves
|
||||||
|
-- module extensions.
|
||||||
|
data ExtensionType
|
||||||
|
= -- | @.ts@ or @.js@.
|
||||||
|
DotTJS
|
||||||
|
| -- | @.tsx@ or @.jsx@.
|
||||||
|
DotTJSX
|
||||||
|
| -- | @.ts@, @.js@, @.tsx@, or @.jsx@.
|
||||||
|
DotAnyTS
|
||||||
|
| -- | @DotExact ext@ is exactly the extension @ext@.
|
||||||
|
DotExact !String
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
-- | Convert an extension (like @\".js\"@) to an 'ExtensionType'.
|
||||||
|
extensionType :: String -> ExtensionType
|
||||||
|
extensionType ".js" = DotTJS
|
||||||
|
extensionType ".jsx" = DotTJSX
|
||||||
|
extensionType ext = DotExact ext
|
||||||
|
|
||||||
|
-- | Split a path into a path with no extension and an extension type. If the
|
||||||
|
-- file has no extension, it gives it 'DotAnyTS' extension type.
|
||||||
|
splitExtensionType :: P.Path b P.File -> (P.Path b P.File, ExtensionType)
|
||||||
|
splitExtensionType file = case P.splitExtension file of
|
||||||
|
Nothing -> (file, DotAnyTS)
|
||||||
|
Just (file', ext) -> (file', extensionType ext)
|
||||||
|
|
||||||
|
-- | The extensions that each 'ExtensionType' represents.
|
||||||
|
allowedExts :: ExtensionType -> [String]
|
||||||
|
allowedExts DotTJS = [".ts", ".js"]
|
||||||
|
allowedExts DotTJSX = [".tsx", ".jsx"]
|
||||||
|
allowedExts DotAnyTS = [".ts", ".js", ".tsx", ".jsx"]
|
||||||
|
allowedExts (DotExact ext) = [ext]
|
||||||
|
|
||||||
|
-- | The 'Eq' instance on this type is slightly weird: it represents compatibilty,
|
||||||
|
-- not equality. Specifically, two extension types are equal if they share at
|
||||||
|
-- least one common extension. This instance is written this way so that the
|
||||||
|
-- "Data.Hashmap" using this as a key (the exports cache) behaves properly when
|
||||||
|
-- extension types are different between the key in the cache and the key in the
|
||||||
|
-- lookup request.
|
||||||
|
instance Eq ExtensionType where
|
||||||
|
DotTJS == DotTJS = True
|
||||||
|
DotTJSX == DotTJSX = True
|
||||||
|
DotAnyTS == DotTJS = True
|
||||||
|
DotAnyTS == DotTJSX = True
|
||||||
|
DotAnyTS == DotAnyTS = True
|
||||||
|
DotExact ext == right = ext `elem` allowedExts right
|
||||||
|
left == right = right == left
|
73
waspc/waspls/src/Wasp/LSP/ExtImport/Syntax.hs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
module Wasp.LSP.ExtImport.Syntax
|
||||||
|
( ExtImportNode (..),
|
||||||
|
findExtImportAroundLocation,
|
||||||
|
getAllExtImports,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Applicative ((<|>))
|
||||||
|
import Text.Read (readMaybe)
|
||||||
|
import Wasp.Analyzer.Parser (ExtImportName (ExtImportField, ExtImportModule))
|
||||||
|
import qualified Wasp.Analyzer.Parser.CST as S
|
||||||
|
import Wasp.Analyzer.Parser.CST.Traverse (Traversal)
|
||||||
|
import qualified Wasp.Analyzer.Parser.CST.Traverse as T
|
||||||
|
import Wasp.LSP.ExtImport.Path (WaspStyleExtFilePath (WaspStyleExtFilePath))
|
||||||
|
import Wasp.LSP.Syntax (findChild, lexemeAt)
|
||||||
|
|
||||||
|
-- | ExtImport syntax node.
|
||||||
|
data ExtImportNode = ExtImportNode
|
||||||
|
{ -- | Location of the 'S.ExtImport' node
|
||||||
|
einLocation :: !Traversal,
|
||||||
|
einName :: !(Maybe ExtImportName),
|
||||||
|
-- | Import path, exactly as it appears in the Wasp source code.
|
||||||
|
einPath :: !(Maybe WaspStyleExtFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
-- | Create a 'ExtImportNode' at a location, assuming the location is at a
|
||||||
|
-- 'S.ExtImport'. It is up to the callee to ensure this.
|
||||||
|
extImportAtLocation :: String -> Traversal -> ExtImportNode
|
||||||
|
extImportAtLocation src location =
|
||||||
|
let maybeName =
|
||||||
|
(ExtImportModule . lexemeAt src <$> findChild S.ExtImportModule location)
|
||||||
|
<|> (ExtImportField . lexemeAt src <$> findChild S.ExtImportField location)
|
||||||
|
maybePathLexeme = lexemeAt src <$> findChild S.ExtImportPath location
|
||||||
|
-- Parse the string lexeme into a Haskell string
|
||||||
|
maybeWaspStylePath = WaspStyleExtFilePath <$> (readMaybe =<< maybePathLexeme)
|
||||||
|
in ExtImportNode
|
||||||
|
{ einLocation = location,
|
||||||
|
einName = maybeName,
|
||||||
|
einPath = maybeWaspStylePath
|
||||||
|
}
|
||||||
|
|
||||||
|
-- | Look for an 'S.ExtImport' node that is either at the given location or is
|
||||||
|
-- an ancestor of the location.
|
||||||
|
--
|
||||||
|
-- For example, passing in a location pointing to an 'S.ExtImportPath' will
|
||||||
|
-- return an 'ExtImportNode' for the parent node of that path node.
|
||||||
|
findExtImportAroundLocation ::
|
||||||
|
-- | Wasp source code.
|
||||||
|
String ->
|
||||||
|
-- | Location to look for external import at.
|
||||||
|
Traversal ->
|
||||||
|
Maybe ExtImportNode
|
||||||
|
findExtImportAroundLocation src location = do
|
||||||
|
extImport <- findExtImportParent location
|
||||||
|
return $ extImportAtLocation src extImport
|
||||||
|
where
|
||||||
|
findExtImportParent t
|
||||||
|
| T.kindAt t == S.ExtImport = Just t
|
||||||
|
| otherwise = T.up t >>= findExtImportParent
|
||||||
|
|
||||||
|
-- | Finds all external imports in a concrete syntax tree.
|
||||||
|
getAllExtImports ::
|
||||||
|
-- | Wasp source code.
|
||||||
|
String ->
|
||||||
|
-- | Syntax forest of the entire Wasp file.
|
||||||
|
[S.SyntaxNode] ->
|
||||||
|
[ExtImportNode]
|
||||||
|
getAllExtImports src syntax = go $ T.fromSyntaxForest syntax
|
||||||
|
where
|
||||||
|
go :: Traversal -> [ExtImportNode]
|
||||||
|
go t = case T.kindAt t of
|
||||||
|
S.ExtImport -> [extImportAtLocation src t]
|
||||||
|
_ -> concatMap go $ T.children t
|
@ -12,12 +12,13 @@ import qualified StrongPath as SP
|
|||||||
import Wasp.Analyzer.Parser.CST.Traverse (Traversal, fromSyntaxForest)
|
import Wasp.Analyzer.Parser.CST.Traverse (Traversal, fromSyntaxForest)
|
||||||
import qualified Wasp.Analyzer.Parser.CST.Traverse as T
|
import qualified Wasp.Analyzer.Parser.CST.Traverse as T
|
||||||
import Wasp.Analyzer.Parser.SourceRegion (sourceSpanToRegion)
|
import Wasp.Analyzer.Parser.SourceRegion (sourceSpanToRegion)
|
||||||
import qualified Wasp.LSP.ExtImport as ExtImport
|
import qualified Wasp.LSP.ExtImport.ExportsCache as ExtImport
|
||||||
import Wasp.LSP.ServerM (HandlerM)
|
import qualified Wasp.LSP.ExtImport.Syntax as ExtImport
|
||||||
|
import Wasp.LSP.ServerMonads (HandlerM)
|
||||||
import qualified Wasp.LSP.ServerState as State
|
import qualified Wasp.LSP.ServerState as State
|
||||||
import Wasp.LSP.Syntax (locationAtOffset, lspPositionToOffset)
|
import Wasp.LSP.Syntax (locationAtOffset, lspPositionToOffset)
|
||||||
import Wasp.LSP.Util (waspSourceRegionToLspRange)
|
import Wasp.LSP.Util (waspSourceRegionToLspRange)
|
||||||
import qualified Wasp.TypeScript as TS
|
import qualified Wasp.TypeScript.Inspect.Exports as TS
|
||||||
|
|
||||||
definitionProviders :: [String -> Traversal -> HandlerM [LSP.LocationLink]]
|
definitionProviders :: [String -> Traversal -> HandlerM [LSP.LocationLink]]
|
||||||
definitionProviders = [extImportDefinitionProvider]
|
definitionProviders = [extImportDefinitionProvider]
|
||||||
|
@ -12,40 +12,22 @@ module Wasp.LSP.Handlers
|
|||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Control.Lens ((.~), (?~), (^.))
|
import Control.Lens ((^.))
|
||||||
import Control.Monad (forM_, when, (<=<))
|
|
||||||
import Control.Monad.IO.Class (liftIO)
|
import Control.Monad.IO.Class (liftIO)
|
||||||
import Control.Monad.Log.Class (logM)
|
import Control.Monad.Log.Class (logM)
|
||||||
import Control.Monad.Reader (asks)
|
|
||||||
import qualified Data.HashMap.Strict as M
|
|
||||||
import Data.List (stripPrefix)
|
|
||||||
import Data.Maybe (isJust, mapMaybe)
|
|
||||||
import Data.Text (Text)
|
|
||||||
import qualified Data.Text as T
|
|
||||||
import Language.LSP.Server (Handlers)
|
import Language.LSP.Server (Handlers)
|
||||||
import qualified Language.LSP.Server as LSP
|
import qualified Language.LSP.Server as LSP
|
||||||
import qualified Language.LSP.Types as LSP
|
import qualified Language.LSP.Types as LSP
|
||||||
import qualified Language.LSP.Types.Lens as LSP
|
import qualified Language.LSP.Types.Lens as LSP
|
||||||
import qualified Language.LSP.VFS as LSP
|
import Wasp.LSP.Analysis (diagnoseWaspFile)
|
||||||
import qualified StrongPath as SP
|
|
||||||
import Wasp.Analyzer (analyze)
|
|
||||||
import Wasp.Analyzer.Parser.ConcreteParser (parseCST)
|
|
||||||
import qualified Wasp.Analyzer.Parser.Lexer as L
|
|
||||||
import Wasp.LSP.Completion (getCompletionsAtPosition)
|
import Wasp.LSP.Completion (getCompletionsAtPosition)
|
||||||
import Wasp.LSP.Debouncer (debounce)
|
import Wasp.LSP.DynamicHandlers (registerDynamicCapabilities)
|
||||||
import Wasp.LSP.Diagnostic (WaspDiagnostic (AnalyzerDiagonstic, ParseDiagnostic), waspDiagnosticToLspDiagnostic)
|
|
||||||
import Wasp.LSP.ExtImport (refreshAllExports, refreshExportsForFiles, updateMissingImportDiagnostics)
|
|
||||||
import Wasp.LSP.GotoDefinition (gotoDefinitionOfSymbolAtPosition)
|
import Wasp.LSP.GotoDefinition (gotoDefinitionOfSymbolAtPosition)
|
||||||
import Wasp.LSP.ServerM (HandlerM, ServerM, handler, modify, sendToReactor)
|
import Wasp.LSP.ServerMonads (ServerM, handler)
|
||||||
import Wasp.LSP.ServerState (cst, currentWaspSource, latestDiagnostics)
|
|
||||||
import qualified Wasp.LSP.ServerState as State
|
|
||||||
import Wasp.LSP.SignatureHelp (getSignatureHelpAtPosition)
|
import Wasp.LSP.SignatureHelp (getSignatureHelpAtPosition)
|
||||||
|
|
||||||
-- LSP notification and request handlers
|
-- | "Initialized" notification is sent when the client is started. We send
|
||||||
|
-- all of our dynamic capability registration requests when this happens.
|
||||||
-- | "Initialized" notification is sent when the client is started. We don't
|
|
||||||
-- have anything we need to do at initialization, but this is required to be
|
|
||||||
-- implemented.
|
|
||||||
--
|
--
|
||||||
-- The client starts the LSP at its own discretion, but commonly this is done
|
-- The client starts the LSP at its own discretion, but commonly this is done
|
||||||
-- either when:
|
-- either when:
|
||||||
@ -57,47 +39,7 @@ import Wasp.LSP.SignatureHelp (getSignatureHelpAtPosition)
|
|||||||
initializedHandler :: Handlers ServerM
|
initializedHandler :: Handlers ServerM
|
||||||
initializedHandler = do
|
initializedHandler = do
|
||||||
LSP.notificationHandler LSP.SInitialized $ \_params -> do
|
LSP.notificationHandler LSP.SInitialized $ \_params -> do
|
||||||
-- Register workspace watcher for src/ directory. This is used for checking
|
registerDynamicCapabilities
|
||||||
-- TS export lists.
|
|
||||||
--
|
|
||||||
-- This can fail if the client doesn't support dynamic registration for this:
|
|
||||||
-- in that case, we can't provide some features. See "Wasp.LSP.ExtImport" for
|
|
||||||
-- what features require this watcher.
|
|
||||||
watchSourceFilesToken <-
|
|
||||||
LSP.registerCapability
|
|
||||||
LSP.SWorkspaceDidChangeWatchedFiles
|
|
||||||
LSP.DidChangeWatchedFilesRegistrationOptions
|
|
||||||
{ _watchers =
|
|
||||||
LSP.List
|
|
||||||
[LSP.FileSystemWatcher {_globPattern = "**/*.{ts,tsx,js,jsx}", _kind = Nothing}]
|
|
||||||
}
|
|
||||||
watchSourceFilesHandler
|
|
||||||
case watchSourceFilesToken of
|
|
||||||
Nothing -> logM "[initializedHandler] Client did not accept WorkspaceDidChangeWatchedFiles registration"
|
|
||||||
Just _ -> logM "[initializedHandler] WorkspaceDidChangeWatchedFiles registered for JS/TS source files"
|
|
||||||
modify (State.regTokens . State.watchSourceFilesToken .~ watchSourceFilesToken)
|
|
||||||
|
|
||||||
-- | Ran when files in src/ change. It refreshes the relevant export lists in
|
|
||||||
-- the cache and updates missing import diagnostics.
|
|
||||||
--
|
|
||||||
-- Both of these tasks are ran in the reactor thread so that other requests
|
|
||||||
-- can still be answered.
|
|
||||||
watchSourceFilesHandler :: LSP.Handler ServerM 'LSP.WorkspaceDidChangeWatchedFiles
|
|
||||||
watchSourceFilesHandler msg = do
|
|
||||||
let (LSP.List uris) = fmap (^. LSP.uri) $ msg ^. LSP.params . LSP.changes
|
|
||||||
logM $ "[watchSourceFilesHandler] Received file changes: " ++ show uris
|
|
||||||
let fileUris = mapMaybe (SP.parseAbsFile <=< stripPrefix "file://" . T.unpack . LSP.getUri) uris
|
|
||||||
forM_ fileUris $ \file -> sendToReactor $ do
|
|
||||||
-- Refresh export list for modified file
|
|
||||||
refreshExportsForFiles [file]
|
|
||||||
-- Update diagnostics for the wasp file
|
|
||||||
updateMissingImportDiagnostics
|
|
||||||
handler $
|
|
||||||
asks (^. State.waspFileUri) >>= \case
|
|
||||||
Just uri -> do
|
|
||||||
logM $ "[watchSourceFilesHandler] Updating missing diagnostics for " ++ show uri
|
|
||||||
publishDiagnostics uri
|
|
||||||
Nothing -> pure ()
|
|
||||||
|
|
||||||
-- | Sent by the client when the client is going to shutdown the server, this
|
-- | Sent by the client when the client is going to shutdown the server, this
|
||||||
-- is where we do any clean up that needs to be done. This cleanup is:
|
-- is where we do any clean up that needs to be done. This cleanup is:
|
||||||
@ -148,83 +90,6 @@ signatureHelpHandler =
|
|||||||
signatureHelp <- handler $ getSignatureHelpAtPosition position
|
signatureHelp <- handler $ getSignatureHelpAtPosition position
|
||||||
respond $ Right signatureHelp
|
respond $ Right signatureHelp
|
||||||
|
|
||||||
-- | Does not directly handle a notification or event, but should be run when
|
-- | Get the 'Uri' from an object that has a 'TextDocument'.
|
||||||
-- text document content changes.
|
|
||||||
--
|
|
||||||
-- It analyzes the document contents and sends any error messages back to the
|
|
||||||
-- LSP client. In the future, it will also store information about the analyzed
|
|
||||||
-- file in "Wasp.LSP.State.State".
|
|
||||||
diagnoseWaspFile :: LSP.Uri -> ServerM ()
|
|
||||||
diagnoseWaspFile uri = do
|
|
||||||
analyzeWaspFile uri
|
|
||||||
|
|
||||||
-- Immediately update import diagnostics only when file watching is enabled
|
|
||||||
sourceWatchingEnabled <- isJust <$> handler (asks (^. State.regTokens . State.watchSourceFilesToken))
|
|
||||||
when sourceWatchingEnabled updateMissingImportDiagnostics
|
|
||||||
|
|
||||||
-- Send diagnostics to client
|
|
||||||
handler $ publishDiagnostics uri
|
|
||||||
|
|
||||||
-- Update exports and missing import diagnostics asynchronously. This is only
|
|
||||||
-- done if file watching is NOT enabled or if the export cache hasn't been
|
|
||||||
-- filled before.
|
|
||||||
exportCacheIsEmpty <- M.null <$> handler (asks (^. State.tsExports))
|
|
||||||
debouncer <- handler $ asks (^. State.debouncer)
|
|
||||||
when (not sourceWatchingEnabled || exportCacheIsEmpty) $
|
|
||||||
debounce debouncer 500000 State.RefreshExports $
|
|
||||||
sendToReactor $ do
|
|
||||||
refreshAllExports
|
|
||||||
updateMissingImportDiagnostics
|
|
||||||
handler $ publishDiagnostics uri
|
|
||||||
|
|
||||||
publishDiagnostics :: LSP.Uri -> HandlerM ()
|
|
||||||
publishDiagnostics uri = do
|
|
||||||
currentDiagnostics <- asks (^. latestDiagnostics)
|
|
||||||
srcString <- asks (^. currentWaspSource)
|
|
||||||
let lspDiagnostics = map (waspDiagnosticToLspDiagnostic srcString) currentDiagnostics
|
|
||||||
LSP.sendNotification
|
|
||||||
LSP.STextDocumentPublishDiagnostics
|
|
||||||
$ LSP.PublishDiagnosticsParams uri Nothing (LSP.List lspDiagnostics)
|
|
||||||
|
|
||||||
analyzeWaspFile :: LSP.Uri -> ServerM ()
|
|
||||||
analyzeWaspFile uri = do
|
|
||||||
modify (State.waspFileUri ?~ uri)
|
|
||||||
|
|
||||||
-- NOTE: we have to be careful to keep CST and source string in sync at all
|
|
||||||
-- times for all threads, so we update them both atomically (via one call to
|
|
||||||
-- 'modify').
|
|
||||||
readSourceString >>= \case
|
|
||||||
Nothing -> do
|
|
||||||
logM $ "Couldn't read source from VFS for wasp file " ++ show uri
|
|
||||||
pure ()
|
|
||||||
Just srcString -> do
|
|
||||||
let (concreteErrorMessages, concreteSyntax) = parseCST $ L.lex srcString
|
|
||||||
-- Atomic update of source string and CST
|
|
||||||
modify ((currentWaspSource .~ srcString) . (cst ?~ concreteSyntax))
|
|
||||||
if not $ null concreteErrorMessages
|
|
||||||
then storeCSTErrors concreteErrorMessages
|
|
||||||
else runWaspAnalyzer srcString
|
|
||||||
where
|
|
||||||
readSourceString = fmap T.unpack <$> readVFSFile uri
|
|
||||||
|
|
||||||
storeCSTErrors concreteErrorMessages = do
|
|
||||||
let newDiagnostics = map ParseDiagnostic concreteErrorMessages
|
|
||||||
modify (latestDiagnostics .~ newDiagnostics)
|
|
||||||
|
|
||||||
runWaspAnalyzer srcString = do
|
|
||||||
let analyzeResult = analyze srcString
|
|
||||||
case analyzeResult of
|
|
||||||
Right _ -> do
|
|
||||||
modify (latestDiagnostics .~ [])
|
|
||||||
Left errs -> do
|
|
||||||
let newDiagnostics = map AnalyzerDiagonstic errs
|
|
||||||
modify (latestDiagnostics .~ newDiagnostics)
|
|
||||||
|
|
||||||
-- | Read the contents of a "Uri" in the virtual file system maintained by the
|
|
||||||
-- LSP library.
|
|
||||||
readVFSFile :: LSP.Uri -> ServerM (Maybe Text)
|
|
||||||
readVFSFile uri = fmap LSP.virtualFileText <$> LSP.getVirtualFile (LSP.toNormalizedUri uri)
|
|
||||||
|
|
||||||
-- | Get the "Uri" from an object that has a "TextDocument".
|
|
||||||
extractUri :: (LSP.HasParams a b, LSP.HasTextDocument b c, LSP.HasUri c LSP.Uri) => a -> LSP.Uri
|
extractUri :: (LSP.HasParams a b, LSP.HasTextDocument b c, LSP.HasUri c LSP.Uri) => a -> LSP.Uri
|
||||||
extractUri = (^. (LSP.params . LSP.textDocument . LSP.uri))
|
extractUri = (^. (LSP.params . LSP.textDocument . LSP.uri))
|
||||||
|
@ -5,6 +5,16 @@ module Wasp.LSP.Reactor
|
|||||||
-- to the LSP client, these tasks are run on the \"reactor thread\". This
|
-- to the LSP client, these tasks are run on the \"reactor thread\". This
|
||||||
-- thread reacts to inputs sent on a 'TChan' and runs the corresponding IO
|
-- thread reacts to inputs sent on a 'TChan' and runs the corresponding IO
|
||||||
-- action.
|
-- action.
|
||||||
|
--
|
||||||
|
-- Essentially, this is a thread pool with only 1 thread. The primary reason
|
||||||
|
-- for this is to make reasoning about concurrent modifications easier, since
|
||||||
|
-- only the reactor thread and the main thread are running.
|
||||||
|
--
|
||||||
|
-- Just 1 thread for these long running tasks should be fine: in general,
|
||||||
|
-- these tasks are triggered by actions from the user who is editing a wasp
|
||||||
|
-- project, which is relatively slow compared to how fast these tasks can
|
||||||
|
-- finish. For tasks that are being triggered often, consider debouncing
|
||||||
|
-- the source to reduce how often it is triggered ("Wasp.LSP.Debouncer").
|
||||||
ReactorInput (..),
|
ReactorInput (..),
|
||||||
reactor,
|
reactor,
|
||||||
startReactorThread,
|
startReactorThread,
|
||||||
|
@ -23,7 +23,7 @@ import Wasp.LSP.Debouncer (newDebouncerIO)
|
|||||||
import Wasp.LSP.Handlers
|
import Wasp.LSP.Handlers
|
||||||
import Wasp.LSP.Reactor (startReactorThread)
|
import Wasp.LSP.Reactor (startReactorThread)
|
||||||
import Wasp.LSP.ServerConfig (ServerConfig)
|
import Wasp.LSP.ServerConfig (ServerConfig)
|
||||||
import Wasp.LSP.ServerM (ServerM, runRLspM)
|
import Wasp.LSP.ServerMonads (ServerM, runRLspM)
|
||||||
import Wasp.LSP.ServerState
|
import Wasp.LSP.ServerState
|
||||||
( RegistrationTokens (RegTokens, _watchSourceFilesToken),
|
( RegistrationTokens (RegTokens, _watchSourceFilesToken),
|
||||||
ServerState (ServerState, _cst, _currentWaspSource, _debouncer, _latestDiagnostics, _reactorIn, _regTokens, _tsExports, _waspFileUri),
|
ServerState (ServerState, _cst, _currentWaspSource, _debouncer, _latestDiagnostics, _reactorIn, _regTokens, _tsExports, _waspFileUri),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
|
|
||||||
module Wasp.LSP.ServerM
|
module Wasp.LSP.ServerMonads
|
||||||
( -- * LSP Server Monads
|
( -- * LSP Server Monads
|
||||||
|
|
||||||
-- The state of the LSP server is used in two different ways:
|
-- The state of the LSP server is used in two different ways:
|
@ -26,12 +26,12 @@ import Data.Hashable (Hashable)
|
|||||||
import GHC.Generics (Generic)
|
import GHC.Generics (Generic)
|
||||||
import qualified Language.LSP.Server as LSP
|
import qualified Language.LSP.Server as LSP
|
||||||
import qualified Language.LSP.Types as LSP
|
import qualified Language.LSP.Types as LSP
|
||||||
import qualified StrongPath as SP
|
|
||||||
import Wasp.Analyzer.Parser.CST (SyntaxNode)
|
import Wasp.Analyzer.Parser.CST (SyntaxNode)
|
||||||
import Wasp.LSP.Debouncer (Debouncer)
|
import Wasp.LSP.Debouncer (Debouncer)
|
||||||
import Wasp.LSP.Diagnostic (WaspDiagnostic)
|
import Wasp.LSP.Diagnostic (WaspDiagnostic)
|
||||||
|
import Wasp.LSP.ExtImport.Path (ExtFileCachePath)
|
||||||
import Wasp.LSP.Reactor (ReactorInput)
|
import Wasp.LSP.Reactor (ReactorInput)
|
||||||
import Wasp.TypeScript (TsExport)
|
import Wasp.TypeScript.Inspect.Exports (TsExport)
|
||||||
|
|
||||||
-- | LSP State preserved between handlers.
|
-- | LSP State preserved between handlers.
|
||||||
--
|
--
|
||||||
@ -59,7 +59,7 @@ data ServerState = ServerState
|
|||||||
}
|
}
|
||||||
|
|
||||||
-- | Map from paths to JS/TS files to the list of exports from that file.
|
-- | Map from paths to JS/TS files to the list of exports from that file.
|
||||||
type TsExportCache = M.HashMap (SP.Path' SP.Abs SP.File') [TsExport]
|
type TsExportCache = M.HashMap ExtFileCachePath [TsExport]
|
||||||
|
|
||||||
-- | LSP dynamic capability registration tokens.
|
-- | LSP dynamic capability registration tokens.
|
||||||
--
|
--
|
||||||
|
@ -62,7 +62,7 @@ The party starts at **9.30 am EDT / 3.30 pm CET** - sign up [here](https://disco
|
|||||||
|
|
||||||
As per usual, there will be memes, swag and lots of interesting dev discussions!
|
As per usual, there will be memes, swag and lots of interesting dev discussions!
|
||||||
|
|
||||||
## Monday: The future is now 🛸
|
## Auto CRUD | Monday: The future is now 🛸
|
||||||
|
|
||||||
<ImgWithCaption
|
<ImgWithCaption
|
||||||
alt="The future is now"
|
alt="The future is now"
|
||||||
@ -75,9 +75,11 @@ That's what we are coming after - is it possible to avoid writing (or generating
|
|||||||
|
|
||||||
**When**: Monday, June 26 2023
|
**When**: Monday, June 26 2023
|
||||||
|
|
||||||
**Read more about it**: coming soon
|
**Read more about it**:
|
||||||
|
- [Twitter thread introing Auto CRUD](https://twitter.com/WaspLang/status/1673376102792806402)
|
||||||
|
- [Docs Guide to Auto CRUD](/docs/guides/crud)
|
||||||
|
|
||||||
## Tuesday: Be real, time 🔌⏱
|
## WebSocket Support | Tuesday: Be real, time 🔌⏱
|
||||||
|
|
||||||
<ImgWithCaption
|
<ImgWithCaption
|
||||||
alt="Realtime"
|
alt="Realtime"
|
||||||
@ -90,7 +92,9 @@ Another situation where you might want to keep things real is when chatting to s
|
|||||||
|
|
||||||
**When**: Tuesday, June 27 2023
|
**When**: Tuesday, June 27 2023
|
||||||
|
|
||||||
**Read more about it**: coming soon
|
**Read more about it**:
|
||||||
|
- [Intro Twitter thread](https://twitter.com/WaspLang/status/1673742264873500673)
|
||||||
|
- [Docs Guide](/docs/guides/websockets)
|
||||||
|
|
||||||
## Wednesday: Community Day 🤗
|
## Wednesday: Community Day 🤗
|
||||||
|
|
||||||
@ -104,9 +108,9 @@ Community is at the centre of Wasp, and Wednesday is at the centre of the week,
|
|||||||
|
|
||||||
**When**: Wednesday, June 28 2023
|
**When**: Wednesday, June 28 2023
|
||||||
|
|
||||||
**Read more about it**: coming soon
|
**Read more about it**: [What can you build with Wasp?](/blog/2023/06/28/what-can-you-build-with-wasp)
|
||||||
|
|
||||||
## Thursday: Take care of your tools 🛠
|
## Wasp LSP 2.0 | Thursday: Take care of your tools 🛠
|
||||||
|
|
||||||
<ImgWithCaption
|
<ImgWithCaption
|
||||||
alt="Tools"
|
alt="Tools"
|
||||||
@ -119,9 +123,9 @@ Us at Wasp, we are pretty much the same as Gimli - we take our tools seriously.
|
|||||||
|
|
||||||
**When**: Thursday, June 29 2023
|
**When**: Thursday, June 29 2023
|
||||||
|
|
||||||
**Read more about it**: coming soon
|
**Read more about it**: [A blog post introing Wasp LSP 2.0](https://wasp-lang.dev/blog/2023/06/29/new-wasp-lsp)
|
||||||
|
|
||||||
## Friday: Waspularity 🤖 + Tutorial-o-thon!
|
## GPT Web App Generator | Friday: Waspularity 🤖 + Tutorial-o-thon!
|
||||||
|
|
||||||
<ImgWithCaption
|
<ImgWithCaption
|
||||||
alt="Waspularity"
|
alt="Waspularity"
|
||||||
@ -134,7 +138,10 @@ To wrap the week up, we'll also start another hackathon, but this time in a bit
|
|||||||
|
|
||||||
**When**: Friday, June 30 2023
|
**When**: Friday, June 30 2023
|
||||||
|
|
||||||
**Read more about it**: coming soon
|
**Read more about it**:
|
||||||
|
- [Intro Twitter thread](https://twitter.com/WaspLang/status/1674814873312608257)
|
||||||
|
- [Try GPT Web App Generator!](https://magic-app-generator.wasp-lang.dev/)
|
||||||
|
- [Join our Tutorial Jam #1 and win prizes!](http://localhost:3002/blog/2023/06/30/tutorial-jam)
|
||||||
|
|
||||||
## Recap
|
## Recap
|
||||||
|
|
||||||
|
1990
web/blog/2023-06-27-build-your-own-twitter-agent-langchain.md
Normal file
107
web/blog/2023-06-28-what-can-you-build-with-wasp.md
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
title: 'What can you build with Wasp?'
|
||||||
|
authors: [matijasos]
|
||||||
|
image: /img/build-with-wasp/build-with-wasp-banner.png
|
||||||
|
tags: [launch-week, showcase]
|
||||||
|
---
|
||||||
|
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||||
|
|
||||||
|
import InBlogCta from './components/InBlogCta';
|
||||||
|
import WaspIntro from './_wasp-intro.md';
|
||||||
|
import ImgWithCaption from './components/ImgWithCaption'
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
alt="Launch Week 3 is coming"
|
||||||
|
source="img/build-with-wasp/build-with-wasp-banner.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
Welcome to the 3rd day of our [Launch Week #3](blog/2023/06/22/wasp-launch-week-three) - Community Day! Our community is the most important aspect of everything we do at Wasp, and we believe it's only right to have a day dedicated to it.
|
||||||
|
|
||||||
|
We'll showcase some of the coolest project built with Wasp so far and through that explore together what kind of apps you can develop with it. Let's dive in!
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
If you're looking for a quick way to start your project, check out our [Ultimate SaaS Starter](https://github.com/wasp-lang/SaaS-Template-GPT). It packs Tailwind, GPT, Stripe ane other popular integrations, all pre-configured for you.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## [CoverLetterGPT.xyz](https://coverlettergpt.xyz/) - GPT-powered cover letter generator
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/build-with-wasp/cover-letter-gpt.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
**Try it out**: [coverlettergpt.xyz](https://coverlettergpt.xyz/)
|
||||||
|
|
||||||
|
**Source code**: https://github.com/vincanger/coverlettergpt
|
||||||
|
|
||||||
|
**Wasp features used**: [Social login with Google + auth UI](/blog/2023/04/12/auth-ui), [email sending](http://localhost:3002/docs/guides/sending-emails)
|
||||||
|
|
||||||
|
**UI Framework**: [Chakra UI](https://chakra-ui.com/)
|
||||||
|
|
||||||
|
Created in the midst of a GPT craze, this is one of the most popular Wasp apps so far! It does exactly what it says on a tin - given job description and your CV, it generates a unique cover letter customized for you. It does that via parsing your CV and feeding it together with the job description to the GPT api, along with the additional settings such as creativity level (careful with that one!).
|
||||||
|
|
||||||
|
Although it started as a fun side project, it seems that people actually find it useful, at least as a starting point for writing your own cover letter. CoverLetterGPT has been used to generate close to 5,000 cover letters!
|
||||||
|
|
||||||
|
Try it out and have fun or use it as an inspiration for your next project!
|
||||||
|
|
||||||
|
## [Amicus.work](https://www.amicus.work/) - most "enterprise SaaS" app 👔 💼
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/build-with-wasp/amicus.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
**Try it out**: [amicus.work](https://www.amicus.work/)
|
||||||
|
|
||||||
|
**Wasp features used**: [Authentication](https://wasp-lang.dev/docs/language/features#authentication--authorization), [email sending](http://localhost:3002/docs/guides/sending-emails), [async/cron jobs](https://wasp-lang.dev/docs/language/features#jobs)
|
||||||
|
|
||||||
|
**UI Framework**: [Material UI](https://mui.com/)
|
||||||
|
|
||||||
|
This app really gives away those "enterprise SaaS" vibes - when you see it you know it means some serious business! The author describes it as "Asana for you lawyers" ([you can read how the author got first customers for it here](blog/2023/02/14/amicus-indiehacker-interview)), or as an easy way for lawyers to manage and collaborate on their workflows.
|
||||||
|
|
||||||
|
File upload, workflow creation, calendar integration, collaboration - this app has it all! Amicus might be the most advanced project made with Wasp so far. Erlis startedbuilding it even with Wasp still in Alpha, and it has withstood the test of time since then.
|
||||||
|
|
||||||
|
## Description Generator - GPT-powered product description generator - first acquired app made with Wasp! 💰💰
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/build-with-wasp/description-generator.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
**Try it out**: [description-generator.online](https://description-generator.online/)
|
||||||
|
|
||||||
|
**Wasp features used**: [Social login with Google + auth UI](/blog/2023/04/12/auth-ui)
|
||||||
|
|
||||||
|
**UI Framework**: [Chakra UI](https://chakra-ui.com/)
|
||||||
|
|
||||||
|
Another SaaS that uses GPT integration to cast its magic! Given product name and instructions on what kind of content you'd like to get, this app generates the professionaly written product listing. It's a perfect fit for marketplace owners that want to present their products in the best light but don't have a budget for the marketing agency.
|
||||||
|
|
||||||
|
What's special about Description Generator is that it was recently sold , making it the first Wasp-powered project that got acquired! Stay tuned, as the whole story is coming soon.
|
||||||
|
|
||||||
|
## TweetBot - your personal Twitter intern! 🐦🤖
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/build-with-wasp/tweet-bot.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
**Try it out**: [banger-tweet-bot.netlify.app](https://banger-tweet-bot.netlify.app/)
|
||||||
|
|
||||||
|
**Source code**: https://github.com/vincanger/banger-tweet-bot
|
||||||
|
|
||||||
|
**Wasp features used**:[Authentication](https://wasp-lang.dev/docs/language/features#authentication--authorization), [async/cron jobs](https://wasp-lang.dev/docs/language/features#jobs)
|
||||||
|
|
||||||
|
**UI Framework**: [Tailwind](https://tailwindcss.com/)
|
||||||
|
|
||||||
|
The latest and greatest from [Vince's](https://twitter.com/hot_town) lab - an app that serves as your personal twitter brainstorming agent! It takes your raw ideas as an input, monitors current twitter trends (from the accounts you selected) and helps you brainstorm new tweets and also drafts them for you!
|
||||||
|
|
||||||
|
While the previously mentioned projects queried the GPT API directly, TweetBot makes use of the [LangChain](https://js.langchain.com/) library, which does a lot of heavy lifting for you, allowing you to produce bigger prompts and preserve the context between subsequent queries.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
As you could see above, Wasp can be used to build pretty much any database-backed web application! It is especially well suited for so called "workflow-based" applications where you typically have a bunch of resources (e.g. your tasks, or tweets) that you want to manipulate in some way.
|
||||||
|
|
||||||
|
With our built-in deployment support (e.g. you can [deploy to Fly.io for free with a single CLI command](https://wasp-lang.dev/docs/deploying)) the whole development process is extremely streamlined.
|
||||||
|
|
||||||
|
We can't wait to see what you build next!
|
||||||
|
|
65
web/blog/2023-06-29-new-wasp-lsp.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
title: 'Wasp LSP 2.0 - Next-level autocompletion and IDE integration for Wasp projects!'
|
||||||
|
authors: [matijasos]
|
||||||
|
image: /img/new-lsp/new-lsp-banner.png
|
||||||
|
tags: [launch-week, product-update]
|
||||||
|
---
|
||||||
|
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||||
|
|
||||||
|
import InBlogCta from './components/InBlogCta';
|
||||||
|
import WaspIntro from './_wasp-intro.md';
|
||||||
|
import ImgWithCaption from './components/ImgWithCaption'
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/new-lsp/new-lsp-banner.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
It's the fourth day of our [Launch Week #3](blog/2023/06/22/wasp-launch-week-three) - today it's all about dev tooling and making sure that the time you spend looking at your IDE is as pleasurable as possible!
|
||||||
|
|
||||||
|
**We present the next generation of Wasp LSP (Language Server Protocol) implementation for [VS Code](https://marketplace.visualstudio.com/items?itemName=wasp-lang.wasp)**! As you might already know, Wasp has its own simple configuration language (`.wasp`) that acts as a glue between your React & Node.js code.
|
||||||
|
|
||||||
|
Although it's a very simple, declarative language (you can think of it as a bit nicer/smarter JSON), and having it allows us to completely tailor the developer experience (aka get rid of boilerplate), it also means we have to provide our own tooling for it (syntax highlighting, auto completion, ...).
|
||||||
|
|
||||||
|
We started with syntax highlighting, then basic autocompletion and snippet support, but now we really took things to the next level! Writing Wasp code now is much closer to what we had in our mind when envisioning Wasp.
|
||||||
|
|
||||||
|
Without further ado, here's what's new:
|
||||||
|
|
||||||
|
## ✨ Autocompletion for config object properties (`auth`, `webSocket`, ...)
|
||||||
|
|
||||||
|
Until now, Wasp offered autocompletion only for the top-level declarations such as `page` or `app`. Now, it works for any (sub)-property (as one would expect 😅)!
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/new-lsp/dict-completion.gif"
|
||||||
|
caption="Fill out your Wasp configuration faster and with less typos! 💻🚀"
|
||||||
|
/>
|
||||||
|
|
||||||
|
## 🔍 Type Hints
|
||||||
|
|
||||||
|
Opening documentation takes you out of your editor and out of your flow. Stay in the zone with in-editor type hints! 💡
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/new-lsp/type-hints.gif"
|
||||||
|
/>
|
||||||
|
|
||||||
|
## 🚨 Import Diagnostics
|
||||||
|
|
||||||
|
Keep tabs on what's left to implement with JS import diagnostics! There's nothing more satisfying than watching those errors vanish. 😌
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/new-lsp/import-diagnostics.gif"
|
||||||
|
caption="Wasp now automatically detects if the function you referenced doesn't exist or is not exported."
|
||||||
|
/>
|
||||||
|
|
||||||
|
## 🔗 Goto Definition
|
||||||
|
|
||||||
|
Your Wasp file is the central hub of your project. Easily navigate your code with goto definition and make changes in a snap! 💨
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/new-lsp/goto-definition.gif"
|
||||||
|
caption="Cmd/Ctrl + click and Wasp LSP takes you straight to the function body!"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
Don't forget to install [Wasp VS Code extension](https://marketplace.visualstudio.com/items?itemName=wasp-lang.wasp) and we wish you happy coding! You can get started right away and [try it out here](/docs/quick-start).
|
114
web/blog/2023-06-30-tutorial-jam.md
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
title: 'Tutorial Jam #1 - Teach Others & Win Prizes!'
|
||||||
|
authors: [vinny]
|
||||||
|
image: /img/tutorial-jam/tutorial-jam-banner.png
|
||||||
|
tags: [launch-week, product-update]
|
||||||
|
---
|
||||||
|
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||||
|
|
||||||
|
import InBlogCta from './components/InBlogCta';
|
||||||
|
import WaspIntro from './_wasp-intro.md';
|
||||||
|
import ImgWithCaption from './components/ImgWithCaption'
|
||||||
|
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/tutorial-jam/tutorial-jam-banner.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
The Wasp Tutorial Jam is a contest where participants are required to create a tutorial about building a fullstack React/Node app with Wasp.
|
||||||
|
|
||||||
|
## Wait, What’s Wasp?
|
||||||
|
|
||||||
|
First of all, it’s sad that you’ve never heard of [Wasp](https://wasp-lang.dev).
|
||||||
|
|
||||||
|
![https://media0.giphy.com/media/kr5PszPQawIRq/giphy.gif?cid=7941fdc6gwgjf866b0akslgciedh53jf9narttadkglvvcp0&ep=v1_gifs_search&rid=giphy.gif&ct=g](https://media0.giphy.com/media/kr5PszPQawIRq/giphy.gif?cid=7941fdc6gwgjf866b0akslgciedh53jf9narttadkglvvcp0&ep=v1_gifs_search&rid=giphy.gif&ct=g)
|
||||||
|
|
||||||
|
Wasp is a unique fullstack framework for building React/NodeJS/Prisma/Tanstack Query apps.
|
||||||
|
|
||||||
|
Because it’s based on a compiler, you write a simple config file, and Wasp can take care of generating the skeleton of your app for you (and regenerating when the config file changes). You can read more about [Wasp here](https://wasp-lang.dev)
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
The rules are simple. The tutorial must:
|
||||||
|
|
||||||
|
- Use Wasp.
|
||||||
|
- Be written in English.
|
||||||
|
- Be original content and not copied from any existing sources.
|
||||||
|
- Be a written tutorial posted to a social blogging platform like [dev.to](http://dev.to) or [hashnode.dev](http://hashnode.dev), or a YouTube video tutorial
|
||||||
|
- Contain the hashtag `#buildwithwasp`
|
||||||
|
- Submitted by pasting the link in the #tutorialjam channel on our [Discord Server](https://discord.gg/rzdnErX)
|
||||||
|
|
||||||
|
AND
|
||||||
|
|
||||||
|
- The tutorial can focus on any topic and be any length (short or long) just as long as it uses Wasp’s fullstack capabilities.
|
||||||
|
|
||||||
|
![https://media1.giphy.com/media/iB4PoTVka0Xnul7UaC/giphy.gif?cid=7941fdc67jeepog7whrdmkbux0c6kxzb8eyhqwpjcd1tunvp&ep=v1_gifs_search&rid=giphy.gif&ct=g](https://media1.giphy.com/media/iB4PoTVka0Xnul7UaC/giphy.gif?cid=7941fdc67jeepog7whrdmkbux0c6kxzb8eyhqwpjcd1tunvp&ep=v1_gifs_search&rid=giphy.gif&ct=g)
|
||||||
|
|
||||||
|
## Judging Criteria
|
||||||
|
|
||||||
|
The judging criteria for the Tutorial Jam will be based on:
|
||||||
|
|
||||||
|
- Clarity and conciseness of the tutorial.
|
||||||
|
- Creativity and originality of the tutorial.
|
||||||
|
- Effectiveness of the tutorial in helping the reader understand and use Wasp to create a fullstack web app or demonstrate a web development topic.
|
||||||
|
|
||||||
|
## Templates & Tutorial Examples
|
||||||
|
|
||||||
|
We have a whole repo of starter templates that you can use with Wasp by [installing wasp](https://wasp-lang.dev/docs/quick-start) and running `wasp new` in the command line. The interactive prompt will ask you what template you’d like to start with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[1] basic (default)
|
||||||
|
Simple starter template with a single page.
|
||||||
|
[2] todo-ts
|
||||||
|
Simple but well-rounded Wasp app implemented with Typescript & full-stack type safety.
|
||||||
|
[3] saas
|
||||||
|
Everything a SaaS needs! Comes with Google auth, ChatGPT API, Tailwind, & Stripe payments.
|
||||||
|
[4] embeddings
|
||||||
|
Comes with code for generating vector embeddings and performing vector similarity search.
|
||||||
|
[5] WaspAI
|
||||||
|
An AI powered code scaffolder. Tell it what kind of app you want and get a scaffolded fullstack app
|
||||||
|
```
|
||||||
|
|
||||||
|
In addition, here are some ideas to help you get inspired. You could build a simple fullstack app with Wasp in order to explain some key concepts:
|
||||||
|
|
||||||
|
- **Wasp’s New AI-Generated App Feature:** build any fullstack app using Wasp’s new AI-generated App feature and explain the process.
|
||||||
|
- *What worked? What didn’t? What are some prompt engineering tips? What did you have to do to get the app in a desired final state?*
|
||||||
|
- **Full-Stack Type Safety:** Using Wasp’s low-on-boilerplate fullstack typesaftey, you could dive deep into types on both frontend and backend.
|
||||||
|
- *How does Wasp’s fullstack typesafety compare to tRPC and/or the T3 stack?*
|
||||||
|
- **Data Management:** With complete control and easy implementation of data models, you could explore the concepts of databases, data management and relational data in a simplified environment.
|
||||||
|
- *What are some tips and tricks for working with Prisma and relational DBs?*
|
||||||
|
- **Understanding Fullstack Web Development:** Wasp being a fullstack tool truly shines a light on how front-end and back-end connect in web development. It’s a great tool for understanding how queries, actions, and other operations in back-end can be utilized in front-end components.
|
||||||
|
- *How does the HTTP protocol work in detail?*
|
||||||
|
|
||||||
|
Or you could write a tutorial that explains how to build:
|
||||||
|
|
||||||
|
- **A vector-powered AI app:** Leverage Wasp’s truly fullstack, serverful architecture to build a personalised tool powered by embeddings and vector stores.
|
||||||
|
- **Realtime Chat or Polling App:** Any realtime app could take advantage of Wasp’s easy-to-use websockets features. The tutorial could explain handling real-time data, and other basic back-end concepts.
|
||||||
|
- **Online Shop:** An e-commerce platform model with features like user registration, product display, a shopping cart, and a check-out process, using Wasp’s easy to configure authorization, and database management.
|
||||||
|
|
||||||
|
## Prizes
|
||||||
|
|
||||||
|
The winners of the Wasp Tutorial Jam will receive the following prizes:
|
||||||
|
|
||||||
|
- First Place: [Wasp-colored mechanical keyboard](https://www.caseking.de/ducky-one-3-yellow-mini-gaming-tastatur-rgb-led-mx-red-us-gata-1745.html?sPartner=999&gclid=Cj0KCQjw1_SkBhDwARIsANbGpFtYpC2-jFuJ94A6VF6oDFLEZQUya3Ky7P9Ih-nU_Zb9NsDjNhmITbIaAtBMEALw_wcB), and your tutorial and info featured on all our blogs (Wasp official website, dev.to, and hashnode)
|
||||||
|
- Second Place: 3 months access to [PluralSight](https://www.pluralsight.com/) courses (tons of Software Development courses, tutorials and lessons!) or a $75 Amazon giftcard, and your tutorial featured on all our blogs
|
||||||
|
- Third Place: Wasp Swag and a feature of your tutorial and info on our social media channels.
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/tutorial-jam/keyboard.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Submission Deadline
|
||||||
|
|
||||||
|
All submissions must be received by Sunday, July 16th 11:59 p.m. CET.
|
||||||
|
Winners will be announced the following week.
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
Head on over to our [Discord Server](https://discord.gg/rzdnErX) and ask away :)
|
||||||
|
|
||||||
|
Good luck!
|
108
web/blog/2023-07-10-gpt-web-app-generator.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
title: 'GPT Web App Generator - Let AI create a full-stack React & Node.js codebase based on your description 🤖🤯'
|
||||||
|
authors: [martinsos]
|
||||||
|
image: /img/gpt-wasp/gpt-wasp-thumbnail.png
|
||||||
|
tags: [wasp-ai, GPT]
|
||||||
|
---
|
||||||
|
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||||
|
|
||||||
|
import ImgWithCaption from './components/ImgWithCaption'
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/gpt-wasp/thumbnail-yellow.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
This project started out as an experiment - we were interested if, given a short description, GPT can generate a full-stack web app in React & Node.js. The results went beyond our expectations!
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
All you have to do in order to use [GPT Web App Generator](https://magic-app-generator.wasp-lang.dev/) is **provide a short description of your app idea in plain English**. You can optionally select your app's brand color and the preferred authentication method (more methods coming soon).
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/gpt-wasp/how-it-works.gif"
|
||||||
|
caption="1. Describe your app 2. Pick the color 3. Generate your app 🚀"
|
||||||
|
/>
|
||||||
|
|
||||||
|
That's it - in a matter of minutes, a full-stack web app codebase, written in React, Node.js, Prisma, and Wasp, will be generated right in front of you, and available for you to download, run it locally and deploy with a single CLI command!
|
||||||
|
|
||||||
|
See a full one-minute demo here:
|
||||||
|
<div className='flex justify-center'>
|
||||||
|
<iframe width="700" height="400" src="https://www.youtube.com/embed/u0MVsPb2MP8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## The stack 📚
|
||||||
|
|
||||||
|
Besides React & Node.js, GPT Web App Generator uses [Prisma](https://www.prisma.io/) and [Wasp](https://github.com/wasp-lang/wasp).
|
||||||
|
|
||||||
|
[Prisma](https://www.prisma.io/) is a type-safe database ORM built on top of PostgreSQL. It makes it easy to deal with data models and database migrations.
|
||||||
|
|
||||||
|
[Wasp](https://github.com/wasp-lang/wasp) is a batteries-included, full-stack framework for React & Node.js. It takes care of everything from front-end to back-end and database along with authentication, sending emails, async jobs, deployment, and more.
|
||||||
|
|
||||||
|
Additionaly, all the code behind GPT Web App Generator is completely open-source: [web app](https://github.com/wasp-lang/wasp/tree/wasp-ai/wasp-ai), [GPT code agent](https://github.com/wasp-lang/wasp/tree/wasp-ai/waspc/src/Wasp/AI).
|
||||||
|
|
||||||
|
## What kind of apps can I build with it?
|
||||||
|
:::caution
|
||||||
|
|
||||||
|
Since this is a GPT-powered project, it's output is not 100% deterministic and small mistakes will sometimes occur in the generated code. For the typical examples of web apps (as seen below) they are usually very minor and straightforward to fix.
|
||||||
|
If you get stuck, [ping us on our Discord](https://discord.gg/rzdnErX).
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
The generated apps are full-stack and consist of front-end, back-end and database. Here are few of the examples we successfully created:
|
||||||
|
|
||||||
|
### My Plants - track your plants' watering schedule 🌱🚰
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/gpt-wasp/my-plants.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
- See the generated code and run it yourself [here](https://magic-app-generator.wasp-lang.dev/result/3bb5dca2-f134-4f96-89d6-0812deab6e0c)
|
||||||
|
|
||||||
|
This app does exactly what it says - makes sure that you water your plants on time! It comes with a fully functioning front-end, back-end and the database with `User` and `Plant` entities. It also features a [full-stack authentication](/blog/2023/04/12/auth-ui) (username & password) and a Tailwind-based design.
|
||||||
|
|
||||||
|
The next step would be to add more advanced features, such as email reminders (via [Wasp email sending support](/docs/guides/sending-emails)) when it is time to water your plant.
|
||||||
|
|
||||||
|
You can see and download the [entire source code](https://magic-app-generator.wasp-lang.dev/result/3bb5dca2-f134-4f96-89d6-0812deab6e0c) and add more features and deploy the app yourself!
|
||||||
|
|
||||||
|
### ToDo app - a classic ✅
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/gpt-wasp/todo-app.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
- See the generated code and run it yourself [here](https://magic-app-generator.wasp-lang.dev/result/07ed440a-3155-4969-b3f5-2031fb1f622f)
|
||||||
|
|
||||||
|
What kind of a demo would this be if it didn't include a ToDo app? GPT Web App Generator successfully scaffolded it, along with all the basic functionality - creating and marking a task as done.
|
||||||
|
|
||||||
|
With the foundations in place (full-stack code, authentication, Tailwind CSS design) you can [see & download the code here](https://magic-app-generator.wasp-lang.dev/result/07ed440a-3155-4969-b3f5-2031fb1f622f) and try it yourself!
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
In order to reduce the complexity and therefore mistakes GPT makes, for this first version of Generator we went with the following limitations regarding generated apps:
|
||||||
|
|
||||||
|
1. No additional npm dependencies.
|
||||||
|
2. No additional files beyond Wasp Pages (React) and Operations (Node). So no additional files with React components, CSS, utility JS, images or similar.
|
||||||
|
3. No TypeScript, just Javascript.
|
||||||
|
4. No advanced Wasp features (e.g. Jobs, Auto CRUD, Websockets, Social Auth, email sending, …).
|
||||||
|
|
||||||
|
## Summary & next steps
|
||||||
|
|
||||||
|
As mentioned above, our goal was to test whether GPT can be effectively used to generate full-stack web applications with React & Node.js. While it's now obvious it can, we have lot of ideas for new features and improvements.
|
||||||
|
|
||||||
|
### Challenges
|
||||||
|
While we were expecting the main issue to be the size of context that GPT has, it turned out to be that the bigger issue is its “smarts”, which determine things like its planning capabilities, capacity to follow provided instructions (we had quite some laughs observing how it sometimes ignores our instructions), and capacity to not do silly mistakes. We saw GPT4 give better results than GPT3.5, but both still make mistakes, and GPT4 is also quite slow/expensive. Therefore we are quite excited about the further developments in the field of AI / LLMs, as they will directly affect the quality of the output for the tools like our Generator.
|
||||||
|
|
||||||
|
### Next features wishlist
|
||||||
|
|
||||||
|
1. Get feedback on this initial experiment - both on the Generator and the Wasp as a framework itself: best place to leave us feedback is on our [Discord](https://discord.com/invite/rzdnErX).
|
||||||
|
2. Further improve code agent & web app.
|
||||||
|
3. Release new version of `wasp` CLI that allows generating new Wasp project by providing short description via CLI. Our code agent will then use GPT to generate project on the disk. This is already ready and should be coming out soon.
|
||||||
|
4. Also allow Wasp users to use code agent for scaffolding specific parts of their Wasp app → you want to add a new Wasp Page (React)? Run our code agent via Wasp CLI or via Wasp vscode extension and have it generated for you, with initial logic already implemented.
|
||||||
|
5. As LLMs progress, try some alternative approaches, e.g. try fine-tuning an LLM with knowledge about Wasp, or give LLM more freedom while generating files and parts of the codebase.
|
||||||
|
6. Write a detailed blog post about how we implemented the Generator, which techniques we used, how we designed our prompts, what worked and what didn’t work, … .
|
||||||
|
|
||||||
|
## Support us! ⭐️
|
||||||
|
|
||||||
|
If you wish to express your support for what we are doing, consider giving us a [star on Github](https://github.com/wasp-lang/wasp)! Everything we do at Wasp is open source, and your support motivates us and helps us to keep making web app development easier and with less boilerplate.
|
@ -10,6 +10,12 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
|
|||||||
|
|
||||||
To enable support for Tailwind in your Wasp project, you simply need to add two config files (`tailwind.config.cjs` and `postcss.config.cjs`) to the root directory. When they are present, Wasp will add the necessary NPM dependencies and copy your config files into the generated project output. You can then start adding [Tailwind CSS directives](https://tailwindcss.com/docs/functions-and-directives#directives) to your CSS files and `className`s to your React components.
|
To enable support for Tailwind in your Wasp project, you simply need to add two config files (`tailwind.config.cjs` and `postcss.config.cjs`) to the root directory. When they are present, Wasp will add the necessary NPM dependencies and copy your config files into the generated project output. You can then start adding [Tailwind CSS directives](https://tailwindcss.com/docs/functions-and-directives#directives) to your CSS files and `className`s to your React components.
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
**After adding the required config files, make sure to restart the local Wasp server** via your CLI (just run `wasp start` again). In some cases it is neccesary for Tailwind to become functional in your app.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
### New project tree overview
|
### New project tree overview
|
||||||
```bash title="tree ." {6,13-14}
|
```bash title="tree ." {6,13-14}
|
||||||
.
|
.
|
||||||
|
@ -2172,7 +2172,7 @@ Any env vars defined in the `.env.server` / `.env.client` files will be forwarde
|
|||||||
console.log(process.env.DATABASE_URL)
|
console.log(process.env.DATABASE_URL)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Database
|
## Database configuration
|
||||||
|
|
||||||
Via `db` field of `app` declaration, you can configure the database used by Wasp.
|
Via `db` field of `app` declaration, you can configure the database used by Wasp.
|
||||||
|
|
||||||
@ -2184,7 +2184,10 @@ app MyApp {
|
|||||||
system: PostgreSQL,
|
system: PostgreSQL,
|
||||||
seeds: [
|
seeds: [
|
||||||
import devSeed from "@server/dbSeeds.js"
|
import devSeed from "@server/dbSeeds.js"
|
||||||
]
|
],
|
||||||
|
prisma: {
|
||||||
|
clientPreviewFeatures: ["extendedWhereUnique"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -2192,7 +2195,7 @@ app MyApp {
|
|||||||
`app.db` is a dictionary with following fields:
|
`app.db` is a dictionary with following fields:
|
||||||
|
|
||||||
#### - `system: DbSystem` (Optional)
|
#### - `system: DbSystem` (Optional)
|
||||||
Database system that Wasp will use. It can be either `PostgreSQL` or `SQLite`.
|
The database system Wasp will use. It can be either `PostgreSQL` or `SQLite`.
|
||||||
If not defined, or even if whole `db` field is not present, default value is `SQLite`.
|
If not defined, or even if whole `db` field is not present, default value is `SQLite`.
|
||||||
If you add/remove/modify `db` field, run `wasp db migrate-dev` to apply the changes.
|
If you add/remove/modify `db` field, run `wasp db migrate-dev` to apply the changes.
|
||||||
|
|
||||||
@ -2200,6 +2203,12 @@ If you add/remove/modify `db` field, run `wasp db migrate-dev` to apply the chan
|
|||||||
Defines seed functions that you can use via `wasp db seed` to seed your database with initial data.
|
Defines seed functions that you can use via `wasp db seed` to seed your database with initial data.
|
||||||
Check out [Seeding](#seeding) section for more details.
|
Check out [Seeding](#seeding) section for more details.
|
||||||
|
|
||||||
|
#### - `prisma: [PrismaOptions]` (Optional)
|
||||||
|
Additional configuration for Prisma.
|
||||||
|
It currently only supports a single field:
|
||||||
|
- `clientPreviewFeatures : string` - allows you to define [Prisma client preview features](https://www.prisma.io/docs/concepts/components/preview-features/client-preview-features).
|
||||||
|
|
||||||
|
|
||||||
### SQLite
|
### SQLite
|
||||||
Default database is `SQLite`, since it is great for getting started with a new project (needs no configuring), but it can be used only in development - once you want to deploy Wasp to production you will need to switch to `PostgreSQL` and stick with it.
|
Default database is `SQLite`, since it is great for getting started with a new project (needs no configuring), but it can be used only in development - once you want to deploy Wasp to production you will need to switch to `PostgreSQL` and stick with it.
|
||||||
Check below for more details on how to migrate from SQLite to PostgreSQL.
|
Check below for more details on how to migrate from SQLite to PostgreSQL.
|
||||||
|
@ -9,7 +9,7 @@ const Announcement = () => {
|
|||||||
let history = useHistory();
|
let history = useHistory();
|
||||||
|
|
||||||
const handleLink = () => {
|
const handleLink = () => {
|
||||||
history.push('/blog/2023/06/22/wasp-launch-week-three')
|
history.push('/blog/2023/06/30/tutorial-jam')
|
||||||
//history.push('/#signup')
|
//history.push('/#signup')
|
||||||
|
|
||||||
//window.open('https://twitter.com/MatijaSosic/status/1646532181324603395')
|
//window.open('https://twitter.com/MatijaSosic/status/1646532181324603395')
|
||||||
@ -39,7 +39,7 @@ const Announcement = () => {
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<span className='item-center flex gap-2 px-3'>
|
<span className='item-center flex gap-2 px-3'>
|
||||||
<span>🔮 Wasp Launch Week #3: Jun 26 - 30</span>
|
<span>📝 Join our Tutorial Jam #1 and win cool prizes!</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className='hidden items-center space-x-2 px-3 lg:flex'>
|
<span className='hidden items-center space-x-2 px-3 lg:flex'>
|
||||||
|
BIN
web/static/img/build-with-wasp/amicus.png
Normal file
After Width: | Height: | Size: 170 KiB |
BIN
web/static/img/build-with-wasp/build-with-wasp-banner.png
Normal file
After Width: | Height: | Size: 157 KiB |
BIN
web/static/img/build-with-wasp/cover-letter-gpt.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
web/static/img/build-with-wasp/description-generator.png
Normal file
After Width: | Height: | Size: 214 KiB |
BIN
web/static/img/build-with-wasp/tweet-bot.png
Normal file
After Width: | Height: | Size: 161 KiB |
BIN
web/static/img/build-your-own-twitter-agent/Untitled 1.png
Normal file
After Width: | Height: | Size: 114 KiB |
BIN
web/static/img/build-your-own-twitter-agent/Untitled 10.png
Normal file
After Width: | Height: | Size: 306 KiB |
BIN
web/static/img/build-your-own-twitter-agent/Untitled 11.png
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
web/static/img/build-your-own-twitter-agent/Untitled 2.png
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
web/static/img/build-your-own-twitter-agent/Untitled 3.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
web/static/img/build-your-own-twitter-agent/Untitled 4.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
web/static/img/build-your-own-twitter-agent/Untitled 5.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
web/static/img/build-your-own-twitter-agent/Untitled 6.png
Normal file
After Width: | Height: | Size: 122 KiB |
BIN
web/static/img/build-your-own-twitter-agent/Untitled 7.png
Normal file
After Width: | Height: | Size: 268 KiB |
BIN
web/static/img/build-your-own-twitter-agent/Untitled 8.png
Normal file
After Width: | Height: | Size: 294 KiB |
BIN
web/static/img/build-your-own-twitter-agent/Untitled 9.png
Normal file
After Width: | Height: | Size: 159 KiB |
BIN
web/static/img/build-your-own-twitter-agent/Untitled.png
Normal file
After Width: | Height: | Size: 211 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 1.9 MiB |
BIN
web/static/img/gpt-wasp/gpt-wasp-thumbnail.png
Normal file
After Width: | Height: | Size: 317 KiB |
BIN
web/static/img/gpt-wasp/how-it-works.gif
Normal file
After Width: | Height: | Size: 12 MiB |
BIN
web/static/img/gpt-wasp/my-plants.png
Normal file
After Width: | Height: | Size: 169 KiB |
BIN
web/static/img/gpt-wasp/thumbnail-yellow.png
Normal file
After Width: | Height: | Size: 199 KiB |
BIN
web/static/img/gpt-wasp/todo-app.png
Normal file
After Width: | Height: | Size: 233 KiB |
BIN
web/static/img/new-lsp/dict-completion.gif
Normal file
After Width: | Height: | Size: 2.2 MiB |
BIN
web/static/img/new-lsp/goto-definition.gif
Normal file
After Width: | Height: | Size: 3.3 MiB |
BIN
web/static/img/new-lsp/import-diagnostics.gif
Normal file
After Width: | Height: | Size: 3.9 MiB |
BIN
web/static/img/new-lsp/new-lsp-banner.png
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
web/static/img/new-lsp/type-hints.gif
Normal file
After Width: | Height: | Size: 4.5 MiB |
BIN
web/static/img/tutorial-jam/keyboard.png
Normal file
After Width: | Height: | Size: 581 KiB |
BIN
web/static/img/tutorial-jam/tutorial-jam-banner.png
Normal file
After Width: | Height: | Size: 1.0 MiB |