Skip to content

Commit 28b3e8c

Browse files
author
Timothy Lindvall
committed
refactor: GUID construction moves to BlockParser.
- The BlockParser is now responsible for constructing a GUID for each Block, if needed. The Block constructor requires a GUID to be passed in. - The BlockFactory is now responsible for ensuring each Block GUID is unique. Once constructed, the BlockParser calls the BlockFactory to register the new GUID. If it's not unique, registering throws an error. - New config option allows for changing the number of significant characters from the MD5 hash used when autogenerating GUIDs. - Update tests to pass in GUID where we construct Blocks. - Also, added some docs for public Block properties.
1 parent 3912811 commit 28b3e8c

File tree

12 files changed

+158
-103
lines changed

12 files changed

+158
-103
lines changed

Diff for: packages/@css-blocks/broccoli/test/Transport.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe("Broccoli Transport Test", () => {
1818
types: {},
1919
collections: {},
2020
});
21-
transport.blocks.add(new Block("foo", "bar"));
21+
transport.blocks.add(new Block("foo", "bar", "test"));
2222
transport.mapping = {} as StyleMapping<"GlimmerTemplates.ResolvedFile">;
2323

2424
transport.reset();

Diff for: packages/@css-blocks/core/src/Analyzer/Analysis.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ export class Analysis<K extends keyof TemplateTypes> {
360360
// Create a temporary block so we can take advantage of `Block.lookup`
361361
// to easily resolve all BlockPaths referenced in the serialized analysis.
362362
// TODO: We may want to abstract this so we're not making a temporary block.
363-
let localScope = new Block("analysis-block", "tmp");
363+
let localScope = new Block("analysis-block", "tmp", "analysis-block");
364364
values.forEach(o => {
365365
analysis.blocks[o.name] = o.block;
366366
localScope.addBlockReference(o.name, o.block);

Diff for: packages/@css-blocks/core/src/BlockParser/BlockFactory.ts

+18
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export class BlockFactory {
4747
private paths: ObjectDictionary<string>;
4848
private preprocessQueue: PromiseQueue<PreprocessJob, ProcessedFile>;
4949

50+
private guids = new Set<string>();
51+
5052
constructor(options: Options, postcssImpl = postcss, faultTolerant = false) {
5153
this.postcssImpl = postcssImpl;
5254
this.configuration = resolveConfiguration(options);
@@ -72,6 +74,7 @@ export class BlockFactory {
7274
this.paths = {};
7375
this.promises = {};
7476
this.blockNames = {};
77+
this.guids.clear();
7578
}
7679

7780
/**
@@ -413,6 +416,21 @@ export class BlockFactory {
413416
}
414417
return preprocessor;
415418
}
419+
420+
/**
421+
* Registers a new GUID with the BlockFactory. If the given GUID has already been used, this
422+
* method will throw an error.
423+
* @param guid - The guid to register.
424+
* @param identifier - A reference to the file this block was generated from, for error reporting.
425+
*/
426+
registerGuid(guid: string, identifier: FileIdentifier) {
427+
if (this.guids.has(guid)) {
428+
throw new CssBlockError("Block uses a GUID that has already been used! Check dependencies for conflicting GUIDs and/or increase the number of significant characters used to generate GUIDs.", {
429+
filename: identifier,
430+
});
431+
}
432+
this.guids.add(guid);
433+
}
416434
}
417435

418436
function sourceMapFromProcessedFile(result: ProcessedFile): RawSourceMap | string | undefined {

Diff for: packages/@css-blocks/core/src/BlockParser/BlockParser.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { importBlocks } from "./features/import-blocks";
2121
import { processDebugStatements } from "./features/process-debug-statements";
2222
import { BlockFactory } from "./index";
2323
import { Syntax } from "./preprocessing";
24+
import { gen_guid } from "./utils/genGuid";
2425

2526
const debug = debugGenerator("css-blocks:BlockParser");
2627

@@ -62,11 +63,17 @@ export class BlockParser {
6263
/**
6364
* Main public interface of `BlockParser`. Given a PostCSS AST, returns a promise
6465
* for the new `Block` object.
65-
* @param root PostCSS AST
66-
* @param sourceFile Source file name
67-
* @param defaultName Name of block
66+
* @param root - PostCSS AST
67+
* @param sourceFile - Source file name
68+
* @param name - Name of block
69+
* @param isDfnFile - Whether the block being parsed is a definition file. Definition files are incomplete blocks
70+
* that will need to merge in rules from its Compiled CSS later. They are also expected to declare
71+
* additional properties that regular Blocks don't, such as `block-id` and `block-interface-index`.
72+
* @param expectedGuid - If a GUID is defined in the file, it's expected to match this value. This argument is only
73+
* relevant to definition files, where the definition file is linked to Compiled CSS and
74+
* both files may declare a GUID.
6875
*/
69-
public async parse(root: postcss.Root, identifier: string, name?: string, isDfnFile = false, expectedId?: string): Promise<Block> {
76+
public async parse(root: postcss.Root, identifier: string, name?: string, isDfnFile = false, expectedGuid?: string): Promise<Block> {
7077
let importer = this.config.importer;
7178
let debugIdent = importer.debugIdentifier(identifier, this.config);
7279
let sourceFile = importer.filesystemPath(identifier, this.config) || debugIdent;
@@ -77,10 +84,11 @@ export class BlockParser {
7784
name = await discoverName(configuration, root, sourceFile, isDfnFile, name);
7885

7986
// Discover the block's GUID, if it's a definition file.
80-
const guid = await discoverGuid(configuration, root, sourceFile, isDfnFile, expectedId);
87+
const guid = await discoverGuid(configuration, root, sourceFile, isDfnFile, expectedGuid) || gen_guid(identifier, configuration.guidAutogenCharacters);
88+
this.factory.registerGuid(guid, identifier);
8189

8290
// Create our new Block object and save reference to the raw AST.
83-
let block = new Block(name, identifier, root, guid);
91+
let block = new Block(name, identifier, guid, root);
8492

8593
if (isDfnFile) {
8694
// Rules only checked when parsing definition files...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as crypto from "crypto";
2+
import * as debugGenerator from "debug";
3+
import * as process from "process";
4+
5+
const DEBUG = debugGenerator("css-blocks:caching");
6+
7+
/**
8+
* Generates a unique identifier from a given input identifier. This
9+
* generated identifier hash will remain consistent for the tuple of (machine,
10+
* user, css blocks installation location).
11+
*
12+
* @param identifier Input Block identifier.
13+
* @param significantChars Number of characters from the start of the hash to use.
14+
* @returns A GUID hash for the given Block identifier, sliced to the number of
15+
* significant characters given.
16+
*/
17+
export function gen_guid(identifier: string, signifcantChars: number): string {
18+
let hash = crypto.createHash("md5")
19+
.update(process.getuid().toString())
20+
.update(__filename)
21+
.update(identifier)
22+
.digest("hex")
23+
.slice(0, signifcantChars);
24+
DEBUG("guid is %s for %s", hash, identifier);
25+
return hash;
26+
}

Diff for: packages/@css-blocks/core/src/BlockTree/Block.ts

+44-61
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { MultiMap, ObjectDictionary } from "@opticss/util";
2-
import * as crypto from "crypto";
3-
import * as debugGenerator from "debug";
42
import {
53
CompoundSelector,
64
ParsedSelector,
75
parseSelector,
86
postcss,
97
postcssSelectorParser as selectorParser,
108
} from "opticss";
11-
import * as process from "process";
129

1310
import { isAttributeNode, isClassNode } from "../BlockParser";
1411
import { isRootNode, toAttrToken } from "../BlockParser";
@@ -22,31 +19,25 @@ import { BlockClass } from "./BlockClass";
2219
import { Inheritable } from "./Inheritable";
2320
import { Styles } from "./Styles";
2421

25-
const DEBUG = debugGenerator("css-blocks:caching");
26-
2722
/**
28-
* Generates a 5 digit, unique identifier from a given input identifier. This
29-
* generated identifier hash will remain consistent for the tuple of (machine,
30-
* user, css blocks installation location).
31-
* @param identifier Input Block identifier.
32-
* @returns This Block's guid hash.
23+
* In-memory representation of a Block. If you're thinking of CSS Blocks
24+
* in relation to the BEM architecture for CSS, this is the... well... "Block".
25+
* Well, with a slight caveat....
26+
*
27+
* The Block is always the root node of a BlockTree. The Block may be the
28+
* parent to any number of BlockClass nodes. Notably, the Block class only
29+
* stores meta information about the block. Any CSS properties assigned to the
30+
* `:scope` selector are stored on a special BlockClass node that is a child of
31+
* the Block. You can access this node directly through the
32+
* `rootClass` property.
33+
*
34+
* Block nodes store all data related to any `@block` imports, the
35+
* `block-name`, implemented Blocks, the inherited Block, and any other
36+
* metadata stored in the Block file.
3337
*/
34-
function gen_guid(identifier: string): string {
35-
let hash = crypto.createHash("md5")
36-
.update(process.getuid().toString())
37-
.update(__filename)
38-
.update(identifier)
39-
.digest("hex")
40-
.slice(0, 5);
41-
DEBUG("guid is %s for %s", hash, identifier);
42-
return hash;
43-
}
44-
4538
export class Block
4639
extends Inheritable<Block, Block, never, BlockClass> {
4740

48-
public static GUIDS_USED = new Map<string, string[]>();
49-
5041
private _blockReferences: ObjectDictionary<Block> = {};
5142
private _blockReferencesReverseLookup: Map<Block, string> = new Map();
5243
private _blockExports: ObjectDictionary<Block> = {};
@@ -56,6 +47,14 @@ export class Block
5647
private _blockErrors: CssBlockError[] = [];
5748
private hasHadNameReset = false;
5849

50+
/**
51+
* A unique identifier for this Block. Generally created from a hash
52+
* of the FileIdentifier and other process information.
53+
*
54+
* For caching to work properly, this GUID must be unique to the block and
55+
* shouldn't change between recompiles. You shouldn't use file contents to
56+
* create this hash.
57+
*/
5958
public readonly guid: string;
6059

6160
/**
@@ -65,52 +64,36 @@ export class Block
6564
*/
6665
private _dependencies: Set<string>;
6766

67+
/**
68+
* A direct reference to the BlockClass that holds style information for the
69+
* `:scope` selector of this Block. The rootClass is also available through
70+
* other traversal methods, as you would access any other BlockClass that
71+
* belongs to this Block.
72+
*/
6873
public readonly rootClass: BlockClass;
74+
75+
/**
76+
* The PostCSS AST of the stylesheet this Block was built from. Used
77+
* primarily for error reporting, if present.
78+
*/
6979
public stylesheet: postcss.Root | undefined;
7080

71-
constructor(name: string, identifier: FileIdentifier, stylesheet?: postcss.Root, incomingGuid?: string) {
81+
/**
82+
* Creates a new Block.
83+
*
84+
* @param name - The default name for this block. This can be reset once (and only once)
85+
* using the `setName()` method.
86+
* @param identifier - An unique ID referencing the file/blob this Block is created from.
87+
* @param guid - The GUID for this block. This GUID should be unique. (BlockFactory is
88+
* responsible for enforcing uniqueness.)
89+
* @param stylesheet - The PostCSS AST of the stylesheet this block is built from.
90+
*/
91+
constructor(name: string, identifier: FileIdentifier, guid: string, stylesheet?: postcss.Root) {
7292
super(name);
7393
this._identifier = identifier;
7494
this._dependencies = new Set<string>();
7595
this.rootClass = new BlockClass(ROOT_CLASS, this);
7696
this.stylesheet = stylesheet;
77-
78-
// If we found a GUID from the :scope rule, use that. Otherwise, generate one.
79-
let guid = incomingGuid || gen_guid(identifier);
80-
81-
// We insist that each block have a unique GUID. In the event that we've somehow
82-
// ended up with two blocks that have the same GUID, our options depend on whether
83-
// we got a preset GUID passed in or not.
84-
if (Block.GUIDS_USED.has(guid)) {
85-
if (incomingGuid) {
86-
// If the GUID was already generated prior to creating the block, we have to
87-
// bail out. (There's likely Compiled CSS that depends on that GUID.)
88-
throw new CssBlockError("Block uses a GUID that has already been used!", {
89-
filename: identifier,
90-
});
91-
} else {
92-
// Ok, we autogenerated this. Let's append some gobbledygook to recover.
93-
const guidBaseList = Block.GUIDS_USED.get(guid);
94-
if (!guidBaseList) {
95-
// Ah crumbs. There should be a list but there isn't.
96-
throw new CssBlockError("Block uses a GUID that has already been used!", {
97-
filename: identifier,
98-
});
99-
}
100-
guid = `${guid}${guidBaseList.length}`;
101-
if (guidBaseList.includes(guid) || Block.GUIDS_USED.has(guid)) {
102-
// Ah crumbs. Our safety check to make sure the GUID really hasn't been used failed.
103-
throw new CssBlockError("Block uses a GUID that has already been used!", {
104-
filename: identifier,
105-
});
106-
}
107-
// Phew, okay, appending a numerical id works. Let's go with that.
108-
guidBaseList.push(guid);
109-
}
110-
} else {
111-
// Hey, this is unique! Cool, let's remember it in case we see it again later.
112-
Block.GUIDS_USED.set(guid, []);
113-
}
11497
this.guid = guid;
11598

11699
this.addClass(this.rootClass);

Diff for: packages/@css-blocks/core/src/configuration/resolver.ts

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const DEFAULTS: ResolvedConfiguration = {
3333
preprocessors: {},
3434
disablePreprocessChaining: false,
3535
maxConcurrentCompiles: 4,
36+
guidAutogenCharacters: 5,
3637
};
3738

3839
/**
@@ -86,6 +87,9 @@ class Resolver implements ResolvedConfiguration {
8687
get maxConcurrentCompiles(): number {
8788
return this._opts.maxConcurrentCompiles;
8889
}
90+
get guidAutogenCharacters(): number {
91+
return this._opts.guidAutogenCharacters;
92+
}
8993
}
9094

9195
export function resolveConfiguration(

Diff for: packages/@css-blocks/core/src/configuration/types.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ export interface Configuration {
3131
* this can be disabled by setting `disablePreprocessChaining` to true.
3232
*/
3333
disablePreprocessChaining: boolean;
34+
35+
/**
36+
* Determines the number of significant characters used when generating GUIDs for blocks.
37+
* These GUIDs are build using a hash based on the file's unique identifier (usually an
38+
* absolute file path).
39+
*
40+
* The default value used is 5 characters. Normally you shouldn't need to change this,
41+
* but you can increase the number of significant characters in the exceedingly rare event
42+
* you run into GUID conflicts.
43+
*
44+
* This value does not affect GUIDs from pre-compiled blocks imported from other dependencies.
45+
* In these cases, the Block ID is pre-determined using the GUID value in the block's
46+
* definition file.
47+
*/
48+
guidAutogenCharacters: number;
3449
}
3550

3651
/**
@@ -50,4 +65,5 @@ export type ConfigurationSimpleKeys = "outputMode"
5065
| "importer"
5166
| "rootDir"
5267
| "disablePreprocessChaining"
53-
| "maxConcurrentCompiles";
68+
| "maxConcurrentCompiles"
69+
| "guidAutogenCharacters";

Diff for: packages/@css-blocks/core/test/Block/inheritable-test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class TestSource extends Inheritable<
6060

6161
type ContainerNode = Inheritable<TestNode, TestSource, TestSource, TestSink>;
6262

63-
const TEST_BLOCK = new Block("test", "tree");
63+
const TEST_BLOCK = new Block("test", "tree", "test");
6464
class TestNode extends Inheritable<
6565
TestNode, // Self
6666
TestSource, // Root

0 commit comments

Comments
 (0)