-
-
Notifications
You must be signed in to change notification settings - Fork 102
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(svelte): add project graph support
- Loading branch information
1 parent
f7c8c72
commit 406ddb7
Showing
10 changed files
with
308 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
packages/svelte/src/generators/utils/add-plugin-to-nx-json.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Tree, writeJson, readNxJson } from '@nx/devkit'; | ||
|
||
export function addPluginToNxJson(pluginName: string, tree: Tree) { | ||
const nxJson = readNxJson(tree); | ||
nxJson.plugins = nxJson.plugins || []; | ||
if (!nxJson.plugins.includes(pluginName)) { | ||
nxJson.plugins.push(pluginName); | ||
} | ||
|
||
writeJson(tree, 'nx.json', nxJson); | ||
} |
162 changes: 162 additions & 0 deletions
162
packages/svelte/src/graph/TypeScriptSvelteImportLocator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import * as path from 'path'; | ||
import type * as ts from 'typescript'; | ||
import { DependencyType, workspaceRoot } from '@nx/devkit'; | ||
import { Scanner } from 'typescript'; | ||
import { stripSourceCode } from 'nx/src/plugins/js/project-graph/build-dependencies/strip-source-code'; | ||
import { readFileSync } from 'fs'; | ||
import { join } from 'path'; | ||
|
||
let tsModule: typeof import('typescript'); | ||
|
||
/** | ||
* This class is originally from TypeScriptImportLocator in nx: | ||
* https://github.com/nrwl/nx/blob/05a9544806e8573bac5eef542a8e8c1b6115dc18/packages/nx/src/project-graph/build-dependencies/typescript-import-locator.ts | ||
*/ | ||
export class TypeScriptSvelteImportLocator { | ||
private readonly scanner: Scanner; | ||
|
||
constructor() { | ||
tsModule = require('typescript'); | ||
this.scanner = tsModule.createScanner(tsModule.ScriptTarget.Latest, false); | ||
} | ||
|
||
fromFile( | ||
filePath: string, | ||
visitor: ( | ||
importExpr: string, | ||
filePath: string, | ||
type: DependencyType | ||
) => void | ||
): void { | ||
const extension = path.extname(filePath); | ||
if (extension !== '.svelte') { | ||
return; | ||
} | ||
const content = readFileSync(join(workspaceRoot, filePath), 'utf-8'); | ||
const strippedContent = stripSourceCode(this.scanner, content); | ||
if (strippedContent !== '') { | ||
const tsFile = tsModule.createSourceFile( | ||
filePath, | ||
strippedContent, | ||
tsModule.ScriptTarget.Latest, | ||
true | ||
); | ||
this.fromNode(filePath, tsFile, visitor); | ||
} | ||
} | ||
|
||
fromNode( | ||
filePath: string, | ||
node: any, | ||
visitor: ( | ||
importExpr: string, | ||
filePath: string, | ||
type: DependencyType | ||
) => void | ||
): void { | ||
if ( | ||
tsModule.isImportDeclaration(node) || | ||
(tsModule.isExportDeclaration(node) && node.moduleSpecifier) | ||
) { | ||
if (!this.ignoreStatement(node)) { | ||
const imp = this.getStringLiteralValue(node.moduleSpecifier); | ||
visitor(imp, filePath, DependencyType.static); | ||
} | ||
return; // stop traversing downwards | ||
} | ||
|
||
if ( | ||
tsModule.isCallExpression(node) && | ||
node.expression.kind === tsModule.SyntaxKind.ImportKeyword && | ||
node.arguments.length === 1 && | ||
tsModule.isStringLiteral(node.arguments[0]) | ||
) { | ||
if (!this.ignoreStatement(node)) { | ||
const imp = this.getStringLiteralValue(node.arguments[0]); | ||
visitor(imp, filePath, DependencyType.dynamic); | ||
} | ||
return; | ||
} | ||
|
||
if ( | ||
tsModule.isCallExpression(node) && | ||
node.expression.getText() === 'require' && | ||
node.arguments.length === 1 && | ||
tsModule.isStringLiteral(node.arguments[0]) | ||
) { | ||
if (!this.ignoreStatement(node)) { | ||
const imp = this.getStringLiteralValue(node.arguments[0]); | ||
visitor(imp, filePath, DependencyType.static); | ||
} | ||
return; | ||
} | ||
|
||
if (node.kind === tsModule.SyntaxKind.PropertyAssignment) { | ||
const name = this.getPropertyAssignmentName( | ||
(node as ts.PropertyAssignment).name | ||
); | ||
if (name === 'loadChildren') { | ||
const init = (node as ts.PropertyAssignment).initializer; | ||
if ( | ||
init.kind === tsModule.SyntaxKind.StringLiteral && | ||
!this.ignoreLoadChildrenDependency(node.getFullText()) | ||
) { | ||
const childrenExpr = this.getStringLiteralValue(init); | ||
visitor(childrenExpr, filePath, DependencyType.dynamic); | ||
return; // stop traversing downwards | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Continue traversing down the AST from the current node | ||
*/ | ||
tsModule.forEachChild(node, (child) => | ||
this.fromNode(filePath, child, visitor) | ||
); | ||
} | ||
|
||
private ignoreStatement(node: ts.Node) { | ||
return stripSourceCode(this.scanner, node.getFullText()) === ''; | ||
} | ||
|
||
private ignoreLoadChildrenDependency(contents: string): boolean { | ||
this.scanner.setText(contents); | ||
let token = this.scanner.scan(); | ||
while (token !== tsModule.SyntaxKind.EndOfFileToken) { | ||
if ( | ||
token === tsModule.SyntaxKind.SingleLineCommentTrivia || | ||
token === tsModule.SyntaxKind.MultiLineCommentTrivia | ||
) { | ||
const start = this.scanner.getStartPos() + 2; | ||
token = this.scanner.scan(); | ||
const isMultiLineCommentTrivia = | ||
token === tsModule.SyntaxKind.MultiLineCommentTrivia; | ||
const end = | ||
this.scanner.getStartPos() - (isMultiLineCommentTrivia ? 2 : 0); | ||
const comment = contents.substring(start, end).trim(); | ||
if (comment === 'nx-ignore-next-line') { | ||
return true; | ||
} | ||
} else { | ||
token = this.scanner.scan(); | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
private getPropertyAssignmentName(nameNode: ts.PropertyName) { | ||
switch (nameNode.kind) { | ||
case tsModule.SyntaxKind.Identifier: | ||
return (nameNode as ts.Identifier).getText(); | ||
case tsModule.SyntaxKind.StringLiteral: | ||
return (nameNode as ts.StringLiteral).text; | ||
default: | ||
return null; | ||
} | ||
} | ||
|
||
private getStringLiteralValue(node: ts.Node): string { | ||
return node.getText().slice(1, -1); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import { | ||
ProjectGraph, | ||
ProjectGraphProcessorContext, | ||
ProjectGraphBuilder, | ||
DependencyType, | ||
ProjectFileMap, | ||
} from '@nx/devkit'; | ||
|
||
import { TypeScriptSvelteImportLocator } from './TypeScriptSvelteImportLocator'; | ||
import { TargetProjectLocator } from 'nx/src/plugins/js/project-graph/build-dependencies/target-project-locator'; | ||
|
||
export type ExplicitDependency = { | ||
sourceProjectName: string; | ||
targetProjectName: string; | ||
sourceProjectFile: string; | ||
type?: DependencyType.static | DependencyType.dynamic; | ||
}; | ||
|
||
export async function processProjectGraph( | ||
graph: ProjectGraph, | ||
context: ProjectGraphProcessorContext | ||
) { | ||
const builder = new ProjectGraphBuilder(graph); | ||
const filesToProcess = context.filesToProcess; | ||
if (Object.keys(filesToProcess).length == 0) { | ||
return graph; | ||
} | ||
|
||
const explicitDependencies: ExplicitDependency[] = | ||
buildExplicitTypeScriptDependencies(graph, filesToProcess); | ||
explicitDependencies.forEach((dependency: ExplicitDependency) => { | ||
builder.addStaticDependency( | ||
dependency.sourceProjectName, | ||
dependency.targetProjectName, | ||
dependency.sourceProjectFile | ||
); | ||
}); | ||
|
||
return builder.getUpdatedProjectGraph(); | ||
} | ||
|
||
export function buildExplicitTypeScriptDependencies( | ||
graph: ProjectGraph, | ||
filesToProcess: ProjectFileMap | ||
) { | ||
function isRoot(projectName: string) { | ||
return graph.nodes[projectName]?.data?.root === '.'; | ||
} | ||
|
||
const importLocator = new TypeScriptSvelteImportLocator(); | ||
const targetProjectLocator = new TargetProjectLocator( | ||
graph.nodes, | ||
graph?.externalNodes || {} | ||
); | ||
|
||
const res: ExplicitDependency[] = []; | ||
Object.keys(filesToProcess).forEach((source) => { | ||
Object.values(filesToProcess[source]).forEach((f) => { | ||
importLocator.fromFile(f.file, (importExpr: any) => { | ||
const target = targetProjectLocator.findProjectWithImport( | ||
importExpr, | ||
f.file | ||
); | ||
let targetProjectName; | ||
if (target) { | ||
if (!isRoot(source) && isRoot(target)) { | ||
// TODO: These edges technically should be allowed but we need to figure out how to separate config files out from root | ||
return; | ||
} | ||
|
||
targetProjectName = target; | ||
} else { | ||
// treat all unknowns as npm packages, they can be eiher | ||
// - mistyped local import, which has to be fixed manually | ||
// - node internals, which should still be tracked as a dependency | ||
// - npm packages, which are not yet installed but should be tracked | ||
targetProjectName = `npm:${importExpr}`; | ||
} | ||
|
||
res.push({ | ||
sourceProjectName: source, | ||
targetProjectName, | ||
sourceProjectFile: f.file, | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
return res; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export { libraryGenerator } from './generators/library/library'; | ||
export { applicationGenerator } from './generators/application/application'; | ||
export { componentGenerator } from './generators/component/component'; | ||
export { processProjectGraph } from './graph/processProjectGraph'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.