Skip to content

Commit

Permalink
feat(svelte): add project graph support
Browse files Browse the repository at this point in the history
  • Loading branch information
DominikPieper committed Aug 22, 2023
1 parent f7c8c72 commit 406ddb7
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 20 deletions.
2 changes: 1 addition & 1 deletion e2e/svelte-e2e/tests/application.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('svelte e2e', () => {
});

describe('Svelte app', () => {
xit('should build svelte application with dependencies', async () => {
it('should build svelte application with dependencies', async () => {
const appName = 'svelteappwithdeps';
await runNxCommandAsync(
`generate @nxext/svelte:app ${appName} --e2eTestRunner='none' --unitTestRunner='none'`
Expand Down
39 changes: 38 additions & 1 deletion e2e/svelte-e2e/tests/library.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { runNxCommandAsync, uniq } from '@nx/plugin/testing';
import { runNxCommandAsync, uniq, updateFile } from '@nx/plugin/testing';
import { createTestProject, installPlugin } from '@nxext/e2e-utils';
import { rmSync } from 'fs';
import { names } from '@nx/devkit';

describe('svelte e2e', () => {
let projectDirectory: string;
Expand Down Expand Up @@ -102,5 +103,41 @@ describe('svelte e2e', () => {
'Storybook builder finished'
);
});

describe('library reference', () => {
it('should create a svelte application with linked lib', async () => {
const projectName = uniq('sveltelinkapp');
const libName = uniq('sveltelinklib');
const libClassName = names(libName).className;

await runNxCommandAsync(
`generate @nxext/svelte:c ${libClassName} --project ${libName}`
);

updateFile(
`apps/${projectName}/src/App.svelte`,
`
<script lang="ts">
import { ${libClassName} } from '@proj/${libName}';
</script>
<main>
<${libClassName} msg="Yey"></${libClassName}>
</main>`
);

await runNxCommandAsync(
`generate @nxext/svelte:app ${projectName} --unitTestRunner='none' --e2eTestRunner='none'`
);
await runNxCommandAsync(
`generate @nxext/svelte:lib ${libName} --buildable --unitTestRunner='none' --e2eTestRunner='none'`
);

const result = await runNxCommandAsync(`build ${projectName}`);
expect(result.stdout).toContain(
`Successfully ran target build for project ${projectName}`
);
});
});
});
});
6 changes: 3 additions & 3 deletions packages/svelte/src/generators/init/init.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Schema } from './schema';
import {
convertNxGenerator,
formatFiles,
GeneratorCallback,
Tree,
Expand All @@ -10,6 +9,7 @@ import { addJestPlugin } from './lib/add-jest-plugin';
import { addCypressPlugin } from './lib/add-cypress-plugin';
import { updateDependencies } from './lib/add-dependencies';
import { addLinterPlugin } from './lib/add-linter-plugin';
import { addPluginToNxJson } from '../utils/add-plugin-to-nx-json';

export async function initGenerator(host: Tree, schema: Schema) {
const tasks: GeneratorCallback[] = [];
Expand All @@ -26,10 +26,10 @@ export async function initGenerator(host: Tree, schema: Schema) {
const installTask = updateDependencies(host);
tasks.push(installTask);

addPluginToNxJson('@nxext/svelte', host);

if (!schema.skipFormat) {
await formatFiles(host);
}
return runTasksInSerial(...tasks);
}

export const initSchematic = convertNxGenerator(initGenerator);
11 changes: 11 additions & 0 deletions packages/svelte/src/generators/utils/add-plugin-to-nx-json.ts
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 packages/svelte/src/graph/TypeScriptSvelteImportLocator.ts
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);
}
}
91 changes: 91 additions & 0 deletions packages/svelte/src/graph/processProjectGraph.ts
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;
}
1 change: 1 addition & 0 deletions packages/svelte/src/index.ts
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';
3 changes: 0 additions & 3 deletions packages/vue/src/generators/component/component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
convertNxGenerator,
formatFiles,
GeneratorCallback,
Tree,
Expand All @@ -25,5 +24,3 @@ export async function componentGenerator(tree: Tree, schema: Schema) {
}

export default componentGenerator;

export const componentSchematic = convertNxGenerator(componentGenerator);
2 changes: 0 additions & 2 deletions packages/vue/src/generators/library/library.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
convertNxGenerator,
ensurePackage,
formatFiles,
GeneratorCallback,
Expand Down Expand Up @@ -91,4 +90,3 @@ export async function libraryGenerator(host: Tree, schema: Schema) {
}

export default libraryGenerator;
export const librarySchematic = convertNxGenerator(libraryGenerator);
Loading

0 comments on commit 406ddb7

Please sign in to comment.