Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/static_shock/lib/src/pipeline.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,23 @@ import 'package:mason_logger/mason_logger.dart';
import 'package:static_shock/src/data.dart';
import 'package:static_shock/src/files.dart';
import 'package:static_shock/src/finishers.dart';
import 'package:static_shock/src/source_files.dart';
import 'package:static_shock/src/templates/components.dart';
import 'package:static_shock/src/templates/layouts.dart';

import 'package:static_shock/src/assets.dart';
import 'package:static_shock/src/pages.dart';
import 'package:static_shock/src/themes.dart';

/// A pipeline that runs a series of steps to generate a static website.
abstract class StaticShockPipeline {
/// Adds the given [SourceFiles] to the collection of source files that are
/// pushed through the pipeline.
///
/// There's only one true source directory, within the project, but users
/// can supplement the collection of source files with extensions.
void addSourceExtension(SourceFiles extensionFiles);

/// Adds the given [picker] to the pipeline, which selects files that will
/// be pushed through the pipeline.
void pick(Picker picker);
Expand All @@ -38,6 +47,10 @@ abstract class StaticShockPipeline {
Set<RemoteFileSource>? pages,
});

/// Loads the given [theme], which is a collection of pages, assets, layouts,
/// and components.
void loadTheme(Theme theme);

/// Adds the given [DataLoader] to the pipeline, which loads external data before
/// any assets or pages are loaded.
void loadData(DataLoader dataLoader);
Expand Down Expand Up @@ -112,6 +125,7 @@ class StaticShockPipelineContext {
required Directory sourceDirectory,
this.buildMode = StaticShockBuildMode.production,
this.cliArguments = const [],
required this.buildCacheDirectory,
required this.errorLog,
Logger? log,
}) : _sourceDirectory = sourceDirectory,
Expand All @@ -123,6 +137,8 @@ class StaticShockPipelineContext {
/// {@macro cli_arguments}
final List<String> cliArguments;

final Directory buildCacheDirectory;

/// The shared [Logger] for all CLI output.
///
/// Plugins should log messages with this [Logger] so that verbosity output level
Expand Down Expand Up @@ -191,6 +207,7 @@ class StaticShockPipelineContext {

/// Returns the [Layout] template from the given file [path].
Layout? getLayout(FileRelativePath path) => _layouts[path];

final _layouts = <FileRelativePath, Layout>{};

/// Adds the given [layout] to the pipeline.
Expand Down
1 change: 1 addition & 0 deletions packages/static_shock/lib/src/plugins/sass.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class SassIndexTransformer implements AssetTransformer {
}

_log.detail("Adding Sass content to cache: ${asset.sourcePath}");
_log.detail("Content:\n${asset.sourceContent!.text}");
_environment.cache["${asset.destinationPath!.filename}.${asset.destinationPath!.extension}"] =
asset.sourceContent!.text!;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/static_shock/lib/src/source_files.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ class SourceFiles {
file,
file.path.substring(directory.path.length),
)) //
.where(
(file) =>
!file.subPath.startsWith("_includes") && //
!file.subPath.startsWith("/_includes"),
)
.where((file) => !_isExcluded(file.subPath))
.where((file) => filter?.passesFilter(file) ?? true);
}
Expand Down
145 changes: 100 additions & 45 deletions packages/static_shock/lib/src/static_shock.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:static_shock/src/infrastructure/data.dart';
import 'package:static_shock/src/infrastructure/timer.dart';
import 'package:static_shock/src/templates/components.dart';
import 'package:static_shock/src/templates/layouts.dart';
import 'package:static_shock/src/themes.dart';
import 'package:yaml/yaml.dart';

import 'package:static_shock/src/assets.dart';
Expand Down Expand Up @@ -47,6 +48,7 @@ class StaticShock implements StaticShockPipeline {
Set<RemoteFile>? remoteDataPickers,
Set<RemoteFile>? remoteAssetPickers,
Set<RemoteFile>? remotePagePickers,
Set<Theme>? themes,
Set<DataLoader>? dataLoaders,
Set<AssetLoader>? assetLoaders,
Set<AssetTransformer>? assetTransformers,
Expand All @@ -68,6 +70,7 @@ class StaticShock implements StaticShockPipeline {
{
const FilePrefixExcluder("."),
},
_themeLoaders = themes ?? {},
_dataLoaders = dataLoaders ?? {},
_assetLoaders = assetLoaders ?? {},
_assetTransformers = assetTransformers ?? {},
Expand Down Expand Up @@ -178,6 +181,14 @@ class StaticShock implements StaticShockPipeline {
late Directory _sourceDirectory;
late SourceFiles _sourceFiles;

@override
void addSourceExtension(SourceFiles extensionFiles) {
_context.log.detail("Adding extension source set: ${extensionFiles.directory.absolute.path}");
_sourceExtensions.add(extensionFiles);
}

final _sourceExtensions = <SourceFiles>{};

late Directory _destinationDir;

/// Adds the given [picker] to the pipeline, which selects files that will
Expand Down Expand Up @@ -213,6 +224,12 @@ class StaticShock implements StaticShockPipeline {
late final Set<RemoteFileSource> _remoteAssets;
late final Set<RemoteFileSource> _remotePages;

/// Loads the given [theme], which is a collection of pages, assets, layouts,
/// and components.
@override
void loadTheme(Theme theme) => _themeLoaders.add(theme);
late final Set<Theme> _themeLoaders;

/// Adds the given [DataLoader] to the pipeline, which loads external data before
/// any assets or pages are loaded.
@override
Expand Down Expand Up @@ -304,7 +321,8 @@ class StaticShock implements StaticShockPipeline {
late ErrorLog _errorLog;
late CheckpointTimer _timer;
late StaticShockPipelineContext _context;
final _files = <FileRelativePath>[];
// final _files = <FileRelativePath>[];
final _files = <SourceFile>[];

/// Generates a static site from content and assets.
///
Expand All @@ -329,12 +347,20 @@ class StaticShock implements StaticShockPipeline {

_clearDestination();

// Ensure the build cache directory exists. This is a cache for intermediate
// artifacts that are used during build, but which are not meant to be
// version controlled, or distributed with the build.
final buildCacheDirectory =
Directory("${_sourceDirectory.path}${path.separator}..${path.separator}.shock${path.separator}build_cache");
buildCacheDirectory.createSync(recursive: true);

//---- Run new pipeline ----
_errorLog = ErrorLog();
_context = StaticShockPipelineContext(
sourceDirectory: _sourceDirectory,
buildMode: _buildMode ?? StaticShockBuildMode.production,
cliArguments: _cliArguments,
buildCacheDirectory: buildCacheDirectory,
errorLog: _errorLog,
log: _log,
);
Expand All @@ -349,6 +375,10 @@ class StaticShock implements StaticShockPipeline {
"basePath": _site.basePath,
});

// Load all themes. Themes will add source files, so this needs to happen
// before picking and/or processing.
await _loadThemes();

// Run plugin configuration - we do this first so that plugins can contribute pickers.
_applyPlugins();

Expand Down Expand Up @@ -484,31 +514,36 @@ class StaticShock implements StaticShockPipeline {

// Load local layouts and components. We do this after loading remove values so
// that local values can overwrite remote values.
for (final sourceFile in _sourceFiles.layouts()) {
_log.detail("Layout: ${sourceFile.subPath}");
_context.putLayout(
Layout(
FileRelativePath.parse(sourceFile.subPath),
sourceFile.file.readAsStringSync(),
),
);
}
for (final sourceFile in _sourceFiles.components()) {
_log.detail("Component: ${sourceFile.subPath}");

final componentContent = front_matter.parse(sourceFile.file.readAsStringSync());

// TODO: process the component data, e.g., pull out CSS imports
_context.putComponent(
path.basenameWithoutExtension(sourceFile.subPath),
Component(
FileRelativePath.parse(sourceFile.subPath),
Map.from(componentContent.data),
// If there's no Front Matter, then `content` will be `null`. In that case, assume
// everything is a Jinja template, and pass the full `value`.
componentContent.content ?? componentContent.value,
),
);
final sourceSets = {_sourceFiles, ..._sourceExtensions};
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be ordered from extensions to source files so source files can overwrite extensions. We already do this in _pickAllSourceFiles(). We should probably create a central method that returns all source sets in that priority order.

for (final sourceSet in sourceSets) {
_log.detail("Processing source set: ${sourceSet.directory}");
for (final sourceFile in sourceSet.layouts()) {
_log.detail("Layout: ${sourceFile.subPath}");
_context.putLayout(
Layout(
FileRelativePath.parse(sourceFile.subPath),
sourceFile.file.readAsStringSync(),
),
);
}

for (final sourceFile in sourceSet.components()) {
_log.detail("Component: ${sourceFile.subPath}");

final componentContent = front_matter.parse(sourceFile.file.readAsStringSync());

// TODO: process the component data, e.g., pull out CSS imports
_context.putComponent(
path.basenameWithoutExtension(sourceFile.subPath),
Component(
FileRelativePath.parse(sourceFile.subPath),
Map.from(componentContent.data),
// If there's no Front Matter, then `content` will be `null`. In that case, assume
// everything is a Jinja template, and pass the full `value`.
componentContent.content ?? componentContent.value,
),
);
}
}

_timer.checkpoint("Load layouts & components", "Finds all layout and component files and loads them into memory");
Expand Down Expand Up @@ -658,23 +693,29 @@ class StaticShock implements StaticShockPipeline {

void _pickAllSourceFiles() {
_log.info("⚡ Picking files");
for (final sourceFile in _sourceFiles.sourceFiles()) {
final relativePath = FileRelativePath.parse(sourceFile.subPath);

pickerLoop:
for (final picker in _pickers) {
if (picker.shouldPick(relativePath)) {
for (final excluder in _excluders) {
if (excluder.shouldExclude(relativePath)) {
break pickerLoop;
// Order sources from extensions to main sources so that main sources
// overwrite extensions (like themes).
final sourceSets = {..._sourceExtensions, _sourceFiles};
for (final sourceSet in sourceSets) {
_log.detail("Picking from source set: ${sourceSet.directory}");
for (final sourceFile in sourceSet.sourceFiles()) {
final relativePath = FileRelativePath.parse(sourceFile.subPath);

pickerLoop:
for (final picker in _pickers) {
if (picker.shouldPick(relativePath)) {
for (final excluder in _excluders) {
if (excluder.shouldExclude(relativePath)) {
break pickerLoop;
}
}
}

_log.detail("Picked: $relativePath");
_files.add(relativePath);
_log.detail("Picked: $relativePath");
_files.add(sourceFile);

// We picked the file. No need to check more pickers.
break;
// We picked the file. No need to check more pickers.
break;
}
}
}
}
Expand All @@ -683,6 +724,18 @@ class StaticShock implements StaticShockPipeline {
_log.info("");
}

Future<void> _loadThemes() async {
_log.info("⚡ Loading themes");

for (final theme in _themeLoaders) {
_log.detail("Loading theme: ${theme.describe}");
await theme.load(this, _context);
}

_timer.checkpoint("Load themes", "Loads all themes, such as from git or the file system");
_log.info("");
}

Future<void> _loadExternalData() async {
_log.info("⚡ Loading external data");

Expand Down Expand Up @@ -755,7 +808,8 @@ class StaticShock implements StaticShockPipeline {
for (final pickedFile in _files) {
late AssetContent content;

final file = _resolveSourceFile(pickedFile);
final file = pickedFile.file;
final relativePath = FileRelativePath.parse(pickedFile.subPath);
try {
// Try to read as plain text, first.
final textContent = file.readAsStringSync();
Expand All @@ -773,12 +827,12 @@ class StaticShock implements StaticShockPipeline {

// Try to interpret the file as a page. If it is a page, load the page.
for (final pageLoader in _pageLoaders) {
if (!pageLoader.canLoad(pickedFile)) {
if (!pageLoader.canLoad(relativePath)) {
continue;
}

_log.detail("Loading page: $pickedFile");
final page = await pageLoader.loadPage(pickedFile, content.text!);
final page = await pageLoader.loadPage(relativePath, content.text!);

final inheritedData = _context.dataIndex.inheritDataForPath(page.sourcePath);
page.data.addEntries({
Expand Down Expand Up @@ -819,11 +873,11 @@ class StaticShock implements StaticShockPipeline {
// The file isn't a page, therefore it must be an asset.
_log.detail("Loading asset: $pickedFile");
_context.addAsset(Asset(
sourcePath: pickedFile,
sourcePath: relativePath,
sourceContent: content,
// By default, we assume a direct copy of each asset. Asset transformers
// can change this decision later.
destinationPath: pickedFile,
destinationPath: relativePath,
destinationContent: content,
));
}
Expand Down Expand Up @@ -987,6 +1041,7 @@ class StaticShock implements StaticShockPipeline {
for (final renderer in _pageRenderers) {
if (renderer.id == rendererId) {
_log.detail("Rendering page '${page.title}' content as '$rendererId'");
_log.detail(" - page path: ${page.pagePath}");
didRender = true;
await renderer.renderContent(_context, page);
}
Expand Down
Loading
Loading