@platformos/platformos-graph 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +83 -0
- package/bin/jsconfig.json +18 -0
- package/bin/platformos-graph +107 -0
- package/dist/getWebComponentMap.d.ts +10 -0
- package/dist/getWebComponentMap.js +66 -0
- package/dist/getWebComponentMap.js.map +1 -0
- package/dist/graph/augment.d.ts +2 -0
- package/dist/graph/augment.js +22 -0
- package/dist/graph/augment.js.map +1 -0
- package/dist/graph/build.d.ts +3 -0
- package/dist/graph/build.js +31 -0
- package/dist/graph/build.js.map +1 -0
- package/dist/graph/module.d.ts +10 -0
- package/dist/graph/module.js +181 -0
- package/dist/graph/module.js.map +1 -0
- package/dist/graph/serialize.d.ts +2 -0
- package/dist/graph/serialize.js +18 -0
- package/dist/graph/serialize.js.map +1 -0
- package/dist/graph/test-helpers.d.ts +33 -0
- package/dist/graph/test-helpers.js +49 -0
- package/dist/graph/test-helpers.js.map +1 -0
- package/dist/graph/traverse.d.ts +14 -0
- package/dist/graph/traverse.js +458 -0
- package/dist/graph/traverse.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/toSourceCode.d.ts +8 -0
- package/dist/toSourceCode.js +76 -0
- package/dist/toSourceCode.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/index.d.ts +11 -0
- package/dist/utils/index.js +47 -0
- package/dist/utils/index.js.map +1 -0
- package/docs/graph.png +0 -0
- package/docs/how-it-works.md +89 -0
- package/fixtures/skeleton/app/views/partials/child.liquid +9 -0
- package/fixtures/skeleton/app/views/partials/parent.liquid +9 -0
- package/fixtures/skeleton/assets/theme.css +0 -0
- package/fixtures/skeleton/assets/theme.js +7 -0
- package/fixtures/skeleton/blocks/_private.liquid +1 -0
- package/fixtures/skeleton/blocks/_static.liquid +10 -0
- package/fixtures/skeleton/blocks/group.liquid +27 -0
- package/fixtures/skeleton/blocks/render-static.liquid +22 -0
- package/fixtures/skeleton/blocks/text.liquid +14 -0
- package/fixtures/skeleton/jsconfig.json +9 -0
- package/fixtures/skeleton/layout/theme.liquid +14 -0
- package/fixtures/skeleton/sections/custom-section.liquid +6 -0
- package/fixtures/skeleton/sections/header-group.json +36 -0
- package/fixtures/skeleton/sections/header.liquid +1 -0
- package/fixtures/skeleton/templates/index.json +20 -0
- package/package.json +41 -0
- package/src/getWebComponentMap.ts +81 -0
- package/src/graph/augment.ts +34 -0
- package/src/graph/build.spec.ts +248 -0
- package/src/graph/build.ts +45 -0
- package/src/graph/module.ts +212 -0
- package/src/graph/serialize.spec.ts +62 -0
- package/src/graph/serialize.ts +20 -0
- package/src/graph/test-helpers.ts +57 -0
- package/src/graph/traverse.ts +639 -0
- package/src/index.ts +5 -0
- package/src/toSourceCode.ts +80 -0
- package/src/types.ts +213 -0
- package/src/utils/index.ts +51 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# How it works
|
|
2
|
+
|
|
3
|
+
## Table of contents
|
|
4
|
+
|
|
5
|
+
1. [What it looks like](#what-it-looks-like)
|
|
6
|
+
1. [Concepts overview](#concepts-overview)
|
|
7
|
+
2. [Algorithm overview](#algorithm-overview)
|
|
8
|
+
|
|
9
|
+
## What it looks like
|
|
10
|
+
|
|
11
|
+
A theme graph looks a bit like this:
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
It's a web of modules and links between them.
|
|
16
|
+
|
|
17
|
+
## Concepts
|
|
18
|
+
|
|
19
|
+
A **Graph** is a set of Nodes and Edges.
|
|
20
|
+
|
|
21
|
+
A **Theme Graph** is a set of *Modules* (nodes) and *References* (edges) defined by an array of *Entry Points*. It has these properties:
|
|
22
|
+
- `rootUri` - root of the theme, e.g. `file:/path/to/theme`
|
|
23
|
+
- `entryPoints` - array of modules that define the theme (all templates and sections)
|
|
24
|
+
- `modules` - all the modules in the theme indexed by URI
|
|
25
|
+
|
|
26
|
+
A **Module** is an object that represents a theme file. It has these properties:
|
|
27
|
+
- `uri` - module identifier, e.g. `file:/path/to/theme/snippets/file.liquid`
|
|
28
|
+
- `type` - e.g. `liquid`, `json`, `javascript`, `css`.
|
|
29
|
+
- `kind` - e.g. `block`, `section`, `snippet`, `template`, etc.
|
|
30
|
+
- `references` - array of *References* that point to this module.
|
|
31
|
+
- `dependencies` - array of *References* that this module depends on.
|
|
32
|
+
|
|
33
|
+
A **Reference** is an object that defines a link between two modules. It has these properties:
|
|
34
|
+
- `source` - a `uri` and `range` that defines which module depends on an other and _where_ in the source file,
|
|
35
|
+
- `target` - a `uri` and `range` that defines which module is being dependended on and optionally what is being depended on in that file,
|
|
36
|
+
- `type` - one of the following:
|
|
37
|
+
- `direct` - the file can't exist without the other, e.g. `{% render 'child' %}`
|
|
38
|
+
- `preset` - the file has a preset that depends on an other file, e.g. a section that has a preset that includes `group` and `text` blocks.
|
|
39
|
+
- `indirect` - the file loosely depends on the other, but not explicilty. e.g. when a file accepts all public theme blocks (`"type": "@theme"`))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Algorithm overview
|
|
43
|
+
|
|
44
|
+
To build the graph, we _traverse_ each module in the set of entry points.
|
|
45
|
+
|
|
46
|
+
When we _traverse_ a module, we do the following:
|
|
47
|
+
- if the module was already visited, we return early, else
|
|
48
|
+
- we extract dependencies from the module's AST, then
|
|
49
|
+
- we _bind_ the module with its dependent modules, then
|
|
50
|
+
- we _traverse_ the dependent modules (it's a recursive algorithm).
|
|
51
|
+
|
|
52
|
+
When we _bind_ a parent module with a child module, we do the following:
|
|
53
|
+
- we add the child module to the parent's dependencies,
|
|
54
|
+
- we add the parent module to the child's references
|
|
55
|
+
|
|
56
|
+
In pseudo code, it looks a bit like this:
|
|
57
|
+
```
|
|
58
|
+
INPUT rootUri
|
|
59
|
+
OUTPUT graph
|
|
60
|
+
|
|
61
|
+
FUNCTION buildGraph(rootUri, entryPoints):
|
|
62
|
+
SET graph = { rootUri, entryPoints, modules: {} }
|
|
63
|
+
FOR module in entryPoints:
|
|
64
|
+
CALL traverse(module, graph)
|
|
65
|
+
ENDFOR
|
|
66
|
+
ENDFUNCTION
|
|
67
|
+
|
|
68
|
+
FUNCTION traverse(module, graph):
|
|
69
|
+
IF graph.modules has module:
|
|
70
|
+
RETURN -- nothing to do, already visited
|
|
71
|
+
ENDIF
|
|
72
|
+
|
|
73
|
+
-- signal that this module has been visited
|
|
74
|
+
SET graph.modules[module.uri] = module
|
|
75
|
+
|
|
76
|
+
EXTRACT references FROM module by visiting the AST
|
|
77
|
+
|
|
78
|
+
FOR reference in references:
|
|
79
|
+
dependency = getModule(reference.target.uri)
|
|
80
|
+
CALL bind(module, dependency, reference)
|
|
81
|
+
CALL traverse(dependency, graph)
|
|
82
|
+
ENDFOR
|
|
83
|
+
ENDFUNCTION
|
|
84
|
+
|
|
85
|
+
FUNCTION bind(parent, child, reference)
|
|
86
|
+
parent.dependencies.push(reference)
|
|
87
|
+
child.references.push(reference)
|
|
88
|
+
ENDFUNCTION
|
|
89
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hoho I'm private
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{% doc %}
|
|
2
|
+
Fixture block component that passes its blocks to the parent snippet
|
|
3
|
+
{% enddoc %}
|
|
4
|
+
|
|
5
|
+
{% capture children %}
|
|
6
|
+
{%- content_for "blocks" -%}
|
|
7
|
+
{% endcapture %}
|
|
8
|
+
|
|
9
|
+
{% render 'parent', children: children %}
|
|
10
|
+
|
|
11
|
+
{% schema %}
|
|
12
|
+
{
|
|
13
|
+
"name": "Group block",
|
|
14
|
+
"blocks": [{ "type": "@theme" }],
|
|
15
|
+
"presets": [
|
|
16
|
+
{
|
|
17
|
+
"name": "Group block",
|
|
18
|
+
"category": "Group",
|
|
19
|
+
"blocks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "text"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
{% endschema %}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{% doc %}
|
|
2
|
+
This block renders the private _static block. Has no schema.
|
|
3
|
+
{% enddoc %}
|
|
4
|
+
|
|
5
|
+
<div class="static-block-wrapper">
|
|
6
|
+
{% content_for 'block',
|
|
7
|
+
type: '_static',
|
|
8
|
+
id: 'static',
|
|
9
|
+
content: 'hello world'
|
|
10
|
+
%}
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
{% schema %}
|
|
14
|
+
{
|
|
15
|
+
"name": "render static block",
|
|
16
|
+
"presets": [
|
|
17
|
+
{
|
|
18
|
+
"name": "Render Static Block"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
{% endschema %}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<html lang="en">
|
|
2
|
+
<head>
|
|
3
|
+
<meta charset="UTF-8">
|
|
4
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5
|
+
<title>Document</title>
|
|
6
|
+
<script type="module" src="{{ 'theme.js' | asset_url }}"></script>
|
|
7
|
+
{{ 'theme.css' | asset_url | stylesheet_tag }}
|
|
8
|
+
{{ content_for_header }}
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
{% sections 'header-group' %}
|
|
12
|
+
{{ content_for_layout }}
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sections": {
|
|
3
|
+
"header": {
|
|
4
|
+
"type": "custom-section",
|
|
5
|
+
"block_order": [
|
|
6
|
+
"header",
|
|
7
|
+
"render-static",
|
|
8
|
+
],
|
|
9
|
+
"blocks": {
|
|
10
|
+
"header": {
|
|
11
|
+
"type": "group",
|
|
12
|
+
"block_order": [
|
|
13
|
+
"text"
|
|
14
|
+
],
|
|
15
|
+
"blocks": {
|
|
16
|
+
"text": {
|
|
17
|
+
"type": "text",
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
"render-static": {
|
|
22
|
+
"type": "render-static",
|
|
23
|
+
"blocks": {
|
|
24
|
+
"static": {
|
|
25
|
+
"type": "_static",
|
|
26
|
+
"static": true,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"order": [
|
|
34
|
+
"header"
|
|
35
|
+
],
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
header content
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@platformos/platformos-graph",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Shopify Theme Graph as a data structure",
|
|
5
|
+
"author": "platformOS",
|
|
6
|
+
"homepage": "https://github.com/Platform-OS/platformos-tools/tree/main/packages/platformos-graph#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/Platform-OS/platformos-tools.git",
|
|
10
|
+
"directory": "packages/platformos-graph"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Platform-OS/platformos-tools/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"platformos-graph": "bin/platformos-graph"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"main": "dist/index.js",
|
|
20
|
+
"types": "dist/index.d.ts",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"@platformos:registry": "https://registry.npmjs.org"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "yarn build:ts",
|
|
27
|
+
"build:ci": "yarn build",
|
|
28
|
+
"build:ts": "tsc -p tsconfig.build.json",
|
|
29
|
+
"type-check": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@platformos/liquid-html-parser": "^0.0.2",
|
|
33
|
+
"@platformos/platformos-check-common": "0.0.2",
|
|
34
|
+
"acorn": "^8.14.1",
|
|
35
|
+
"acorn-walk": "^8.3.4",
|
|
36
|
+
"vscode-uri": "^3.0.7"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@platformos/platformos-check-node": "0.0.2"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { path, recursiveReadDirectory } from '@platformos/platformos-check-common';
|
|
2
|
+
import { ancestor as visit } from 'acorn-walk';
|
|
3
|
+
import { Dependencies, Void, WebComponentMap } from './types';
|
|
4
|
+
import { CallExpression } from 'acorn';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Regular expression for web component names
|
|
8
|
+
*
|
|
9
|
+
* Based on the HTML specification for valid custom element names.
|
|
10
|
+
*
|
|
11
|
+
* https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
|
|
12
|
+
*/
|
|
13
|
+
const wcre =
|
|
14
|
+
/^[a-z][-.\d_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*([-][-.\d_a-z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)$/u;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Find all the web component definitions from the JavaScript files in the
|
|
18
|
+
* assets directory.
|
|
19
|
+
*
|
|
20
|
+
* From those, we'll be able to map `<custom-element-name>` to the definition in
|
|
21
|
+
* the corresponding asset file.
|
|
22
|
+
*/
|
|
23
|
+
export async function getWebComponentMap(
|
|
24
|
+
rootUri: string,
|
|
25
|
+
{ fs, getSourceCode }: Pick<Dependencies, 'fs' | 'getSourceCode'>,
|
|
26
|
+
): Promise<WebComponentMap> {
|
|
27
|
+
const webComponentDefs: WebComponentMap = new Map();
|
|
28
|
+
const assetRoot = path.join(rootUri, 'assets');
|
|
29
|
+
const jsFiles = await recursiveReadDirectory(fs, assetRoot, ([fileName]) =>
|
|
30
|
+
fileName.endsWith('.js'),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
await Promise.all(
|
|
34
|
+
jsFiles.map((uri) =>
|
|
35
|
+
findWebComponentReferences(uri, assetRoot, getSourceCode, webComponentDefs),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return webComponentDefs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function findWebComponentReferences(
|
|
43
|
+
uri: string,
|
|
44
|
+
assetRoot: string,
|
|
45
|
+
getSourceCode: Dependencies['getSourceCode'],
|
|
46
|
+
result: WebComponentMap,
|
|
47
|
+
): Promise<Void> {
|
|
48
|
+
const sourceCode = await getSourceCode(uri);
|
|
49
|
+
if (sourceCode.type !== 'javascript') {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ast = sourceCode.ast;
|
|
54
|
+
if (ast instanceof Error) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const node of ast.body) {
|
|
59
|
+
visit(node, {
|
|
60
|
+
Literal(node, _state, ancestors) {
|
|
61
|
+
if (typeof node.value === 'string' && wcre.test(node.value)) {
|
|
62
|
+
// Making sure we're looking at customElements.define calls
|
|
63
|
+
const parentNode = ancestors.at(-2);
|
|
64
|
+
if (!parentNode) return;
|
|
65
|
+
if (parentNode.type !== 'CallExpression') return;
|
|
66
|
+
const callee = (parentNode as CallExpression).callee;
|
|
67
|
+
if (callee.type !== 'MemberExpression') return;
|
|
68
|
+
const property = callee.property;
|
|
69
|
+
if (property.type !== 'Identifier') return;
|
|
70
|
+
if (property.name !== 'define') return;
|
|
71
|
+
|
|
72
|
+
result.set(node.value, {
|
|
73
|
+
assetName: path.relative(uri, assetRoot),
|
|
74
|
+
range: [property.start, node.end],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
memoize,
|
|
3
|
+
memo,
|
|
4
|
+
recursiveReadDirectory as findAllFiles,
|
|
5
|
+
path,
|
|
6
|
+
} from '@platformos/platformos-check-common';
|
|
7
|
+
import { toSourceCode } from '../toSourceCode';
|
|
8
|
+
import { AugmentedDependencies, IDependencies } from '../types';
|
|
9
|
+
import { identity } from '../utils';
|
|
10
|
+
|
|
11
|
+
export function augmentDependencies(rootUri: string, ideps: IDependencies): AugmentedDependencies {
|
|
12
|
+
return {
|
|
13
|
+
fs: ideps.fs,
|
|
14
|
+
getBlockSchema: memoize(ideps.getBlockSchema, identity),
|
|
15
|
+
getSectionSchema: memoize(ideps.getSectionSchema, identity),
|
|
16
|
+
|
|
17
|
+
// parse at most once
|
|
18
|
+
getSourceCode: memoize(
|
|
19
|
+
ideps.getSourceCode ??
|
|
20
|
+
async function defaultGetSourceCode(uri) {
|
|
21
|
+
const contents = await ideps.fs.readFile(uri);
|
|
22
|
+
return toSourceCode(uri, contents);
|
|
23
|
+
},
|
|
24
|
+
identity,
|
|
25
|
+
),
|
|
26
|
+
|
|
27
|
+
getWebComponentDefinitionReference: ideps.getWebComponentDefinitionReference,
|
|
28
|
+
getThemeBlockNames: memo(() =>
|
|
29
|
+
findAllFiles(ideps.fs, path.join(rootUri, 'blocks'), ([uri]) => uri.endsWith('.liquid')).then(
|
|
30
|
+
(uris) => uris.map((uri) => path.basename(uri, '.liquid')),
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
};
|
|
34
|
+
}
|